【実践記事・ADV編⑦】立ち絵・背景をUnityに組み込む——キャラクター表示とシーン切り替えの実装

初心者向け実践開発

対応バージョン:Unity 6.3 LTS(6000.3系)/WebGL出力 この記事はシリーズ全11回の第7回です。第6回では19本の会話データをDialogueDataに入力し、台本の矛盾を修正しました。今回はゲームの見た目を作る工程——立ち絵・背景の取り込みとタイトル画面の実装に進みます。 【PR】本記事にはアフィリエイトリンクを含みます。

目次


はじめに

前回までで、DialogueDataを19本、TopicDataを15個、CharacterDataを4つ、すべてInspectorへ手入力しました。この量を毎回手入力するのは非効率で、入力ミスも起きやすくなります。

そこで今回はまず、CSVファイルからこれらのScriptableObjectを一括生成するエディタ拡張を作り、入力の手間を減らします。そのうえで、AIで生成したキャラクター立ち絵と背景画像をUnityに取り込み、会話シーンに表示する実装を行います。

この記事でやることは以下の5つです。

  • CSVから会話データ・話題カード・キャラクターデータを一括インポートする仕組みを作る
  • 立ち絵・背景・UI素材をUnityにインポートする
  • ゲーム画面に立ち絵と背景を配置する
  • 会話の進行に合わせて立ち絵・背景を切り替える仕組みを追加する
  • タイトル画面シーンを作る

1. 会話データ・話題カード・キャラクターデータをCSVから一括インポートする仕組みを作る

なぜCSVインポートが必要か

DialogueData・TopicData・CharacterDataはすべてScriptableObjectで、これまではUnityのInspectorに直接入力していました。しかし数が増えると(今回は合計38個)、Inspectorでの手入力は時間がかかるうえ、コピー&ペーストのミスにも気づきにくくなります。

CSVファイルであれば表計算ソフトでまとめて編集でき、セリフの誤字や設定漏れも一覧で見直せます。今回は「CSVを読み込んでScriptableObjectを自動生成するエディタ拡張」を2本作ります。エディタ拡張とは、Unityエディタ自体に独自のツールやウィンドウを追加する仕組みのことです。

CSV用のフォルダを準備する

🔧 詳しい手順

  1. ProjectウィンドウのAssetsフォルダで右クリック→「Create」→「Folder」で「Data/CSV」フォルダを作成する
  2. Data/CSVフォルダ内に「Topics」フォルダと「Characters」フォルダを作成する
Assets/Data/CSV/
├── (DialogueData用のCSVファイルを直接置く)
├── Topics/       // TopicData用のCSVファイル
└── Characters/   // CharacterData用のCSVファイル

DialogueDataをCSVから読み込むエディタ拡張を作る

Scripts/フォルダ以下のEditorフォルダ(Editorという名前のフォルダに置いたスクリプトはビルドに含まれず、エディタ上でのみ動作します)に「DialogueDataImporter」という名前でMonoBehaviour Scriptを作成し、以下の内容に書き換えます。

using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections.Generic;

public class DialogueDataImporter : EditorWindow
{
    private string csvPath = "Assets/Data/CSV/";
    private string outputPath = "Assets/Data/Dialogues/";

    [MenuItem("MysteryADV/DialogueData CSVインポート")]
    public static void ShowWindow()
    {
        GetWindow<DialogueDataImporter>("DialogueData インポート");
    }

    void OnGUI()
    {
        GUILayout.Label("DialogueData CSVインポーター", EditorStyles.boldLabel);
        GUILayout.Space(10);

        GUILayout.Label("CSVフォルダ(Assets以下)");
        csvPath = GUILayout.TextField(csvPath);

        GUILayout.Label("出力フォルダ(Assets以下)");
        outputPath = GUILayout.TextField(outputPath);

        GUILayout.Space(10);

        if (GUILayout.Button("CSVフォルダ内の全CSVをインポート"))
        {
            ImportAll();
        }
    }

    void ImportAll()
    {
        if (!Directory.Exists(csvPath))
        {
            EditorUtility.DisplayDialog("エラー", "CSVフォルダが見つかりません:" + csvPath, "OK");
            return;
        }

        if (!Directory.Exists(outputPath))
            Directory.CreateDirectory(outputPath);

        string[] csvFiles = Directory.GetFiles(csvPath, "*.csv");

        if (csvFiles.Length == 0)
        {
            EditorUtility.DisplayDialog("エラー", "CSVファイルが見つかりません", "OK");
            return;
        }

        int successCount = 0;

        foreach (string filePath in csvFiles)
        {
            if (ImportCSV(filePath))
                successCount++;
        }

        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();

        EditorUtility.DisplayDialog("完了", successCount + "件のDialogueDataを生成しました", "OK");
    }

    bool ImportCSV(string filePath)
    {
        string[] lines = File.ReadAllLines(filePath, System.Text.Encoding.UTF8);

        if (lines.Length < 2)
        {
            Debug.LogWarning("空のCSVをスキップ: " + filePath);
            return false;
        }

        // ヘッダー行をスキップ、2行目以降を読む
        // フォーマット: sceneTitle,setFlagOnComplete,speakerName,dialogueText,isMonologue

        string sceneTitle = "";
        string setFlag = "";
        List<DialogueLine> lineList = new List<DialogueLine>();

        for (int i = 1; i < lines.Length; i++)
        {
            string line = lines[i].Trim();
            if (string.IsNullOrEmpty(line)) continue;

            string[] cols = SplitCSVLine(line);

            if (cols.Length < 5)
            {
                Debug.LogWarning("列数が不足している行をスキップ(" + (i + 1) + "行目): " + line);
                continue;
            }

            // 最初のデータ行からsceneTitle・setFlagを取得
            if (sceneTitle == "")
            {
                sceneTitle = cols[0].Trim();
                setFlag = cols[1].Trim();
            }

            DialogueLine dl = new DialogueLine();
            dl.speakerName = cols[2].Trim();
            dl.dialogueText = cols[3].Trim();
            dl.isMonologue = cols[4].Trim().ToLower() == "true";

            lineList.Add(dl);
        }

        if (lineList.Count == 0 || sceneTitle == "")
        {
            Debug.LogWarning("有効なデータがありません: " + filePath);
            return false;
        }

        // ScriptableObject生成
        DialogueData data = ScriptableObject.CreateInstance<DialogueData>();
        data.name = sceneTitle;
        data.sceneTitle = sceneTitle;
        data.setFlagOnComplete = setFlag;
        data.lines = lineList.ToArray();

        string assetPath = outputPath + sceneTitle + ".asset";

        if (AssetDatabase.LoadAssetAtPath<DialogueData>(assetPath) != null)
            AssetDatabase.DeleteAsset(assetPath);

        AssetDatabase.CreateAsset(data, assetPath);
        Debug.Log("新規作成: " + assetPath);

        return true;
    }

    // カンマ区切りCSVのパース(ダブルクォート対応)
    string[] SplitCSVLine(string line)
    {
        List<string> result = new List<string>();
        bool inQuotes = false;
        System.Text.StringBuilder current = new System.Text.StringBuilder();

        for (int i = 0; i < line.Length; i++)
        {
            char c = line[i];

            if (c == '"')
            {
                if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
                {
                    // エスケープされたダブルクォート("")
                    current.Append('"');
                    i++;
                }
                else
                {
                    inQuotes = !inQuotes;
                }
            }
            else if (c == ',' && !inQuotes)
            {
                result.Add(current.ToString());
                current.Clear();
            }
            else
            {
                current.Append(c);
            }
        }

        result.Add(current.ToString());
        return result.ToArray();
    }
}

SplitCSVLine()はカンマ区切りのCSVを1行ずつ分割する処理です。セリフの中にカンマが含まれる場合(「はい、そうです」など)は、CSV側でそのセル全体をダブルクォートで囲むことで正しく分割されます。

CSVフォーマットsceneTitle,setFlagOnComplete,speakerName,dialogueText,isMonologue

以下は沙耶との「あの夜のこと」(Ch2_Saya_Night)の例です。

sceneTitle,setFlagOnComplete,speakerName,dialogueText,isMonologue
Ch2_Saya_Night,talked_saya_night,凛,昨夜、月凪に伺いました。少しお話を聞かせてもらえますか。,false
Ch2_Saya_Night,,沙耶,……何のお話かしら。,false
Ch2_Saya_Night,,凛,(本心を見せない目。何かを隠している。),true

sceneTitlesetFlagOnCompleteはコード上、1行目のデータ行しか読まれません。2行目以降は空欄のままで構いません。

🔧 詳しい手順(使い方)

  1. Assets/Data/CSV/フォルダに、DialogueDataの数だけCSVファイルを用意する(ファイル名は何でもよい。アセット名はCSV内のsceneTitle列から決まる)
  2. Unityメニューの「MysteryADV」→「DialogueData CSVインポート」を開く
  3. CSVフォルダ・出力フォルダのパスを確認し、「CSVフォルダ内の全CSVをインポート」ボタンを押す
  4. 「〇件のDialogueDataを生成しました」と表示されれば成功
DialogueDataImporter

⚠️ 注意:証拠の設定はCSVに含まれない このCSVフォーマットにはgrantEvidenceOnComplete(証拠付与)の列がありません。動画などの証拠を入手する会話(Ch3_Matsumoto_Videoなど)は、CSVインポート後にInspectorでGrant Evidence On Completeを個別に設定する必要があります。

TopicData・CharacterDataをCSVから読み込むエディタ拡張を作る

同じくEditorフォルダに「TopicCharacterImporter2」という名前でMonoBehaviour Scriptを作成し、以下の内容に書き換えます。TopicDataとCharacterDataは参照関係にあるため、1つのウィンドウに2つのインポート機能をまとめています。

using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections.Generic;

public class TopicCharacterImporter2 : EditorWindow
{
    private string topicCSVPath = "Assets/Data/CSV/Topics/";
    private string topicOutputPath = "Assets/Resources/Topics/";
    private string charCSVPath = "Assets/Data/CSV/Characters/";
    private string charOutputPath = "Assets/Data/Characters/";

    [MenuItem("MysteryADV/TopicData・CharacterData CSVインポート2")]
    public static void ShowWindow()
    {
        GetWindow<TopicCharacterImporter2>("Topic・Character インポート2");
    }

    void OnGUI()
    {
        GUILayout.Label("TopicData・CharacterData CSVインポーター", EditorStyles.boldLabel);
        GUILayout.Space(10);

        GUILayout.Label("--- TopicData ---", EditorStyles.boldLabel);
        GUILayout.Label("CSVフォルダ");
        topicCSVPath = GUILayout.TextField(topicCSVPath);
        GUILayout.Label("出力フォルダ");
        topicOutputPath = GUILayout.TextField(topicOutputPath);

        GUILayout.Space(10);

        GUILayout.Label("--- CharacterData ---", EditorStyles.boldLabel);
        GUILayout.Label("CSVフォルダ");
        charCSVPath = GUILayout.TextField(charCSVPath);
        GUILayout.Label("出力フォルダ");
        charOutputPath = GUILayout.TextField(charOutputPath);

        GUILayout.Space(10);

        if (GUILayout.Button("TopicDataをインポート"))
            ImportAllTopics();

        GUILayout.Space(5);

        if (GUILayout.Button("CharacterDataをインポート"))
            ImportAllCharacters();
    }

    // =====================
    // TopicData インポート
    // =====================
    void ImportAllTopics()
    {
        if (!Directory.Exists(topicCSVPath))
        {
            EditorUtility.DisplayDialog("エラー", "CSVフォルダが見つかりません:" + topicCSVPath, "OK");
            return;
        }
        if (!Directory.Exists(topicOutputPath))
            Directory.CreateDirectory(topicOutputPath);

        string[] csvFiles = Directory.GetFiles(topicCSVPath, "*.csv");
        if (csvFiles.Length == 0)
        {
            EditorUtility.DisplayDialog("エラー", "CSVファイルが見つかりません", "OK");
            return;
        }

        int count = 0;
        foreach (string filePath in csvFiles)
        {
            if (ImportTopicCSV(filePath)) count++;
        }

        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
        EditorUtility.DisplayDialog("完了", count + "件のTopicDataを生成しました", "OK");
    }

    bool ImportTopicCSV(string filePath)
    {
        string[] lines = File.ReadAllLines(filePath, System.Text.Encoding.UTF8);
        if (lines.Length < 2) return false;

        string[] cols = SplitCSVLine(lines[1].Trim());
        if (cols.Length < 6) return false;

        string topicID           = cols[0].Trim();
        string topicTitle        = cols[1].Trim();
        string dialoguePath      = cols[2].Trim();
        bool   isUnlocked        = cols[3].Trim().ToLower() == "true";
        string unlockFlagID      = cols[4].Trim();
        string setFlagOnComplete = cols[5].Trim();

        DialogueData dialogue = null;
        if (!string.IsNullOrEmpty(dialoguePath))
        {
            string[] guids = AssetDatabase.FindAssets(dialoguePath + " t:DialogueData");
            if (guids.Length > 0)
                dialogue = AssetDatabase.LoadAssetAtPath<DialogueData>(AssetDatabase.GUIDToAssetPath(guids[0]));
            if (dialogue == null)
                Debug.LogWarning("DialogueDataが見つかりません: " + dialoguePath);
        }

        string fileName  = Path.GetFileNameWithoutExtension(filePath);
        string assetPath = topicOutputPath + fileName + ".asset";

        // 既存があれば削除
        if (AssetDatabase.LoadAssetAtPath<TopicData>(assetPath) != null)
            AssetDatabase.DeleteAsset(assetPath);

        TopicData data       = ScriptableObject.CreateInstance<TopicData>();
        data.name            = fileName;
        data.topicID         = topicID;
        data.topicTitle      = topicTitle;
        data.dialogue        = dialogue;
        data.unlockFlagID    = unlockFlagID;
        data.setFlagOnComplete = setFlagOnComplete;

        AssetDatabase.CreateAsset(data, assetPath);
        Debug.Log("作成: " + assetPath);
        return true;
    }

    // =====================
    // CharacterData インポート
    // =====================
    void ImportAllCharacters()
    {
        if (!Directory.Exists(charCSVPath))
        {
            EditorUtility.DisplayDialog("エラー", "CSVフォルダが見つかりません:" + charCSVPath, "OK");
            return;
        }
        if (!Directory.Exists(charOutputPath))
            Directory.CreateDirectory(charOutputPath);

        string[] csvFiles = Directory.GetFiles(charCSVPath, "*.csv");
        if (csvFiles.Length == 0)
        {
            EditorUtility.DisplayDialog("エラー", "CSVファイルが見つかりません", "OK");
            return;
        }

        int count = 0;
        foreach (string filePath in csvFiles)
        {
            if (ImportCharacterCSV(filePath)) count++;
        }

        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
        EditorUtility.DisplayDialog("完了", count + "件のCharacterDataを生成しました", "OK");
    }

    bool ImportCharacterCSV(string filePath)
    {
        string[] lines = File.ReadAllLines(filePath, System.Text.Encoding.UTF8);
        if (lines.Length < 2) return false;

        string characterID   = "";
        string characterName = "";
        List<TopicData> topics = new List<TopicData>();

        for (int i = 1; i < lines.Length; i++)
        {
            string line = lines[i].Trim();
            if (string.IsNullOrEmpty(line)) continue;

            string[] cols = SplitCSVLine(line);
            if (cols.Length < 3) continue;

            if (characterID == "")
            {
                characterID   = cols[0].Trim();
                characterName = cols[1].Trim();
            }

            string topicName = cols[2].Trim();
            string[] guids = AssetDatabase.FindAssets(topicName + " t:TopicData");
            if (guids.Length > 0)
            {
                TopicData topic = AssetDatabase.LoadAssetAtPath<TopicData>(AssetDatabase.GUIDToAssetPath(guids[0]));
                if (topic != null) topics.Add(topic);
                else Debug.LogWarning("TopicDataが見つかりません: " + topicName);
            }
            else
            {
                Debug.LogWarning("TopicDataが見つかりません: " + topicName);
            }
        }

        if (characterID == "") return false;

        string fileName  = Path.GetFileNameWithoutExtension(filePath);
        string assetPath = charOutputPath + fileName + ".asset";

        // 既存があれば削除
        if (AssetDatabase.LoadAssetAtPath<CharacterData>(assetPath) != null)
            AssetDatabase.DeleteAsset(assetPath);

        CharacterData data  = ScriptableObject.CreateInstance<CharacterData>();
        data.name           = fileName;
        data.characterID    = characterID;
        data.characterName  = characterName;
        data.topics         = topics.ToArray();

        AssetDatabase.CreateAsset(data, assetPath);
        Debug.Log("作成: " + assetPath);
        return true;
    }

    string[] SplitCSVLine(string line)
    {
        List<string> result = new List<string>();
        bool inQuotes = false;
        System.Text.StringBuilder current = new System.Text.StringBuilder();

        for (int i = 0; i < line.Length; i++)
        {
            char c = line[i];
            if (c == '"')
            {
                if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
                { current.Append('"'); i++; }
                else inQuotes = !inQuotes;
            }
            else if (c == ',' && !inQuotes)
            { result.Add(current.ToString()); current.Clear(); }
            else
                current.Append(c);
        }
        result.Add(current.ToString());
        return result.ToArray();
    }
}

TopicDataのCSVフォーマットtopicID,topicTitle,dialoguePath,isUnlocked,unlockFlagID,setFlagOnComplete(1ファイルにつきデータ行は1行だけ)

topicID,topicTitle,dialoguePath,isUnlocked,unlockFlagID,setFlagOnComplete
saya_night,あの夜のこと,Ch2_Saya_Night,true,,talked_saya_night

このCSVファイルはTopic_Saya_Night.csvのように保存します。生成されるTopicDataのアセット名は、CSVの中身ではなくCSVファイル名から決まります(コード内のPath.GetFileNameWithoutExtension(filePath)の部分)。

CharacterDataのCSVフォーマットcharacterID,characterName,topicName(キャラクターが持つ話題の数だけ行を繰り返す)

characterID,characterName,topicName
saya,桐島沙耶,Topic_Saya_Night
saya,桐島沙耶,Topic_Saya_Tanaka
saya,桐島沙耶,Topic_Saya_Leave
saya,桐島沙耶,Topic_Saya_Past

topicName列(3列目)はAssetDatabase.FindAssets(topicName + " t:TopicData")でTopicDataアセットを名前検索するために使われます。先ほど作ったTopicDataのCSVファイル名と、この列の値を完全に一致させてください。一致しないと「TopicDataが見つかりません」という警告が出て、その話題はキャラクターに紐づきません。

🔧 詳しい手順(使い方・実行順序に注意)

  1. Assets/Data/CSV/Topics/フォルダに、TopicDataの数だけCSVファイルを用意する
  2. Assets/Data/CSV/Characters/フォルダに、CharacterDataの数だけCSVファイルを用意する
  3. Unityメニューの「MysteryADV」→「TopicData・CharacterData CSVインポート2」を開く
  4. 先に「TopicDataをインポート」ボタンを押す
  5. 完了したら「CharacterDataをインポート」ボタンを押す

④⑤の順番は必ず守ってください。 CharacterDataのインポートはTopicDataを名前で検索する仕組みのため、TopicDataが先に存在していないと、すべての話題が空のまま登録されてしまいます。同様に、TopicDataのインポートもDialogueDataを名前検索するため、DialogueDataを先にインポートしておく必要があります。

実行順序:DialogueDataインポート → TopicDataインポート → CharacterDataインポート

⚠️ ハマったポイント

  • isUnlocked列がTopicDataに反映されない。 ImportTopicCSV()内ではbool isUnlocked = cols[3]...という変数にCSVの値を読み込んでいますが、生成するTopicDataオブジェクトに代入する行がありません。そのためCSVでfalseを指定しても、TopicDataのIs UnlockedはScriptableObjectの初期値であるtrueのままになります。最初から話題が全部見えてしまい、しばらく原因に気づけませんでした。反映させるには、data.unlockFlagID = unlockFlagID;の下にdata.isUnlocked = isUnlocked;を1行追加するか、インポート後にInspectorで手動チェックを外す必要があります
  • 再インポートすると既存アセットは削除され、上書きされる。 インポーター側でAssetDatabase.DeleteAsset()のあとにAssetDatabase.CreateAsset()を実行しているため、Inspectorで直接手を加えた内容があっても再インポートで消えます。修正はCSV側に反映してから再インポートしてください
  • CharacterDataを先にインポートすると全滅する。 実行順序を間違えてCharacterDataを先にインポートすると、TopicDataが見つかりませんという警告がコンソールに大量に出て、すべてのキャラクターがtopicsが空の状態で生成されます。この場合はTopicDataを先にインポートしてから、CharacterDataだけ再インポートすれば直ります

2. 画像素材のフォルダ構成とインポート

フォルダを作る

まずUnityのProjectウィンドウで素材用のフォルダを作成します。

Assets/
├── Sprites/
│   ├── Characters/   ← キャラクター立ち絵
│   ├── Backgrounds/  ← 背景画像
│   └── UI/           ← ボタンなどのUI素材

インポート設定が重要な理由

画像をUnityに取り込むだけでは正しく表示されません。Texture Typeなどのインポート設定を用途に合わせて変更する必要があります。

設定を間違えると、立ち絵の透過が効かなかったり、背景がぼやけたりします。

立ち絵のインポート設定

キャラクター立ち絵(背景透過PNG)を選択してInspectorで以下を設定し、Applyを押します。

項目理由
Texture TypeSprite (2D and UI)UIとして使用するため
Alpha Is Transparency✅ ON透過PNGを正しく表示するため
CompressionNone品質劣化を防ぐため
Max Size2048高解像度立ち絵に対応するため

特にAlpha Is Transparencyは忘れやすい設定です。これをONにしないと、透過部分が黒や白で塗りつぶされてしまいます。

背景画像のインポート設定

背景画像も同様にSprite (2D and UI)に設定しますが、1点だけ異なります。

項目
Texture TypeSprite (2D and UI)
Generate Mip Maps☐ OFF

Generate Mip MapsはOFFにします。背景は常に一定のサイズで表示されるため、距離に応じて解像度を変えるミップマップは不要です。ONのままだとメモリを無駄に使います。


3. ゲーム画面への配置

Canvasの設定——画面サイズ対応の鍵

Canvas(UI全体の親)の設定は、ゲームの見た目に大きく影響します。

HierarchyのCanvasを選択してInspectorで以下を設定します。

項目
Render ModeScreen Space – Overlay
UI Scale ModeScale With Screen Size
Reference ResolutionX: 1920 / Y: 1080
Screen Match ModeMatch Width Or Height
Match0.5

Scale With Screen Sizeが重要です。 これを設定することで、ブラウザやウィンドウのサイズが変わっても自動的にUIがスケールされます。WebGL出力では特に重要な設定です。

Hierarchyの並び順——重なりの仕組み

CanvasではHierarchyの下にあるオブジェクトが手前に表示されます。以下の順番になるよう配置してください。

Canvas
├── BackgroundImage    ← 一番奥(背景)
├── CharacterImage     ← 立ち絵
├── DialoguePanel      ← テキストボックス
├── TopicPanel         ← 話題カード
├── NotificationPanel  ← 通知
└── EndingPanel        ← エンディング

背景が一番上(奥)、テキストボックスや話題カードが一番下(手前)になっていれば正解です。

CharacterImageの配置

立ち絵を表示するImageオブジェクトを追加します。Canvasを右クリック → UI → Image → 名前をCharacterImageに変更します。

項目
AnchorBottom Center
Pivot X / Y0.5 / 0
Preserve Aspect✅ ON
Raycast Target☐ OFF

Preserve AspectをONにすることで、立ち絵の縦横比が崩れずに表示されます。Raycast TargetはOFFにしてください。ONのままだと、後でキャラクタークリックを実装したときに背景のRaycastが立ち絵に遮られてしまいます。


4. 会話に合わせて立ち絵・背景を切り替える

セリフごとに画像を差し替える仕組み

ADVゲームでは、セリフが進むたびに立ち絵の表情や背景が変わります。この仕組みを実装するために、DialogueLine.csに立ち絵と背景のフィールドを追加します。

public Sprite characterSprite;  // このセリフで表示する立ち絵(nullなら前の状態を維持)
public Sprite backgroundSprite; // このセリフで切り替える背景(nullなら変更なし)

nullのままにしておけば変化しません。切り替えたいセリフだけに設定すればよいので、全セリフに設定する必要はありません。

DialogueManagerで切り替える

ShowLine()に立ち絵と背景の切り替え処理を追加します。考え方は以下の通りです。

セリフを表示するたびにShowLine()が呼ばれる
 ↓
backgroundSpriteが設定されていれば背景を切り替え
 ↓
characterSpriteが設定されていれば立ち絵を切り替えて表示
 ↓
characterSpriteがnullなら立ち絵を非表示(または前の状態を維持)

DialogueDataへの設定

各DialogueDataのInspectorを開き、背景が切り替わる最初のセリフ行のBackground Spriteに対応する背景画像をドラッグ&ドロップします。

背景の使い分けは以下を参考にしてください。

会話データ背景場所
Ch1_Openingbg_ferry_deckフェリー甲板
Ch1_Loungebg_loungeラウンジ「月凪」
Ch1_Morningbg_inn_corridor民宿・廊下
Ch2_Matsumoto_*bg_inn_corridor民宿・廊下
Ch2_Saya_*bg_loungeラウンジ「月凪」
Ch2_Tanaka_*bg_police_station駐在所
Ch2_Isobe_*bg_harbor_edge港の端

5. タイトル画面シーンを作る

なぜタイトル画面を別シーンにするか

ゲーム起動時の最初の画面をタイトル画面にするには、Unityのシーン管理を使います。Build ProfilesのScene List(シーン一覧)の0番目のシーンが起動時に表示されるため、TitleSceneを0番に設定します。

シーンの作成

File → New Scene → Basic (Built-in) → Createでシーンを新規作成し、TitleSceneという名前でAssets/Scenes/に保存します。

タイトル画面の構成

タイトル画面は最低限以下の要素があればOKです。

  • 背景・ロゴを含むタイトル画像(全画面Image)
  • スタートボタン(透明なButtonを重ねる)

スタートボタンはImageコンポーネントのAlphaを0にして透明にし、タイトル画像上のボタン部分に重ねます。テキストや枠線はタイトル画像に描き込んでおく方がデザインの自由度が高くなります。

シーン遷移スクリプト

スタートボタンをクリックしたときにゲームシーンへ遷移するスクリプトを作ります。

using UnityEngine.SceneManagement;

public class TitleSceneController : MonoBehaviour
{
    public void OnStartButtonClicked()
    {
        SceneManager.LoadScene("SampleScene");
    }
}

このスクリプトをCanvasにアタッチし、スタートボタンのOnClick()にOnStartButtonClickedを登録します。

Build ProfilesへのTitleScene追加

File → Build Profilesを開いて、TitleSceneとSampleSceneが両方登録されているか確認します。TitleSceneを一番上(インデックス0)に配置してください。


6. 動作確認チェックリスト

  • [ ] CSVインポーターでDialogueData・TopicData・CharacterDataが正しく生成される
  • [ ] タイトル画面が表示される
  • [ ] スタートボタンをクリックするとSampleSceneに遷移する
  • [ ] 立ち絵が透過付きで正しく表示される
  • [ ] 背景が正しく表示される
  • [ ] NEXTボタンを押すと背景・立ち絵が切り替わる
  • [ ] Consoleにエラーが出ていない

まとめ

今回はCSV一括インポートの仕組みを作り、Inspectorへの手入力から解放されました。あわせてゲームの見た目も大きく改善されました。

CSVインポーターは「読み込む→ScriptableObjectを作る→既存を上書きする」という単純な仕組みですが、TopicData→CharacterDataのように他のアセットを名前で参照するインポート順序には注意が必要です。

画像のインポート設定は地味な作業ですが、Alpha Is TransparencyRaycast Targetなど、設定を間違えると後で思わぬトラブルの原因になります。最初にしっかり設定しておきましょう。

次回は、オープニング会話から話題カードが表示されるまでの流れと、フラグ連動で話題が自動解放される仕組みの実装を解説します。


あわせて読みたいおすすめ書籍【PR】

今回実装した画像インポート・Canvas設定・立ち絵の切り替えについてさらに学びたい方には、以下の書籍もおすすめです。本セクションにはアフィリエイトリンクを含みます。

※このセクションのリンクはアフィリエイトリンクであり、商品が購入された場合、サイト運営者に紹介料が支払われることがあります【PR】。


この記事を作成するにあたって参考にしたサイト

コメント

タイトルとURLをコピーしました