Unityテキストアドベンチャー開発:技術資料

1. プロジェクト概要

このプロジェクトは、Unityでのテキストアドベンチャーゲームの実装を目的としています。以下の要素を含み、プレイヤーの選択に応じてストーリーが分岐する構造です:

  • 背景画像:シーンに応じた背景画像を表示
  • キャラクター情報:キャラクターの名前、表情、感情を表示
  • サウンド情報:BGM、ボイス、効果音(SE)を再生
  • アセットの動的ロード:Addressableを利用し、アセットを効率的に管理

2. データ構造とExcelでの管理

Excel(またはCSVファイル)を使って、各シーンの情報を管理します。以下のような構成でデータを管理します。

Excelの構造例

idbackgroundspeakercharacter_namecharacter_emotiontextchoice1_textchoice1_nextIdchoice2_textchoice2_nextIdbgmvoicesound_effect
1forestキャラクター1リサhappy「こんにちは!森へようこそ!」続ける2帰る3forest_bgmlisa_happyse_birds
2forestキャラクター2ジョンneutral「ここで何をしているんだ?」話をする4無視する3forest_bgmjohn_neutralse_step
3townあなたは町に戻った。town_bgm
  • id:各シーンのID
  • background:背景画像の名前
  • speaker:話すキャラクターの識別子
  • character_name:キャラクターの名前
  • character_emotion:キャラクターの感情(表情)
  • text:会話テキスト
  • choiceX_textchoiceX_nextId:選択肢と次の会話ノードID
  • bgm:シーンで流すBGMの名前
  • voice:キャラクターのボイスの名前
  • sound_effect:効果音の名前

3. Addressableの設定

  • Addressablesのインストール
    • Unityの「Window」→「Package Manager」から「Addressables」をインストール。
  • アセットの登録
    • 背景、キャラクター画像、サウンド(BGM、ボイス、効果音)をアセットとしてAddressablesに登録。
    • 登録したいアセットに「Addressable」にチェックを入れ、適切なラベルを付与します。
  • ビルド
    • 「Window」→「Asset Management」→「Addressables」→「Groups」でAddressablesウィンドウを開き、「Build」→「New Build」→「Default Build Script」でビルド。

4. スクリプトの実装

DialogueLoader(CSVデータの読み込み)

DialogueLoaderスクリプトでCSVファイルを読み込み、各シーン情報をDictionaryとして管理します。

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

public class DialogueLoader : MonoBehaviour
{
    public TextAsset csvFile;
    private Dictionary<int, DialogueNode> dialogueData;

    void Start()
    {
        LoadDialogueData();
    }

    void LoadDialogueData()
    {
        dialogueData = new Dictionary<int, DialogueNode>();
        using (StringReader reader = new StringReader(csvFile.text))
        {
            bool isFirstLine = true;

            while (reader.Peek() > -1)
            {
                string line = reader.ReadLine();
                if (isFirstLine) { isFirstLine = false; continue; }

                string[] columns = line.Split(',');
                DialogueNode node = new DialogueNode
                {
                    id = int.Parse(columns[0]),
                    background = columns[1],
                    speaker = columns[2],
                    characterName = columns[3],
                    characterEmotion = columns[4],
                    text = columns[5],
                    bgm = columns[10],
                    voice = columns[11],
                    soundEffect = columns[12],
                    choices = new List<DialogueNode.Choice>()
                };

                if (!string.IsNullOrEmpty(columns[6]))
                {
                    node.choices.Add(new DialogueNode.Choice { text = columns[6], nextId = int.Parse(columns[7]) });
                }
                if (!string.IsNullOrEmpty(columns[8]))
                {
                    node.choices.Add(new DialogueNode.Choice { text = columns[8], nextId = int.Parse(columns[9]) });
                }

                dialogueData[node.id] = node;
            }
        }
    }

    public DialogueNode GetNode(int id)
    {
        return dialogueData.ContainsKey(id) ? dialogueData[id] : null;
    }
}

[System.Serializable]
public class DialogueNode
{
    public int id;
    public string background;
    public string speaker;
    public string characterName;
    public string characterEmotion;
    public string text;
    public string bgm;
    public string voice;
    public string soundEffect;
    public List<Choice> choices;

    [System.Serializable]
    public class Choice
    {
        public string text;
        public int nextId;
    }
}

DialogueManager(データを使って表示とサウンド再生)

DialogueManagerスクリプトで、Addressablesを利用してアセットをロードし、シーン情報に応じた表示やサウンド再生を行います。

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using System.Threading.Tasks;

public class DialogueManager : MonoBehaviour
{
    public DialogueLoader dialogueLoader;
    public Text dialogueText;
    public Text characterNameText;
    public Image background;
    public Image characterImage;
    public Button[] choiceButtons;

    public AudioSource bgmSource;
    public AudioSource voiceSource;
    public AudioSource soundEffectSource;

    private DialogueNode currentNode;

    void Start()
    {
        SetDialogue(1);
    }

    public void SetDialogue(int id)
    {
        currentNode = dialogueLoader.GetNode(id);
        if (currentNode == null) return;

        dialogueText.text = currentNode.text;
        characterNameText.text = currentNode.characterName;

        // Addressableでアセットを非同期にロード
        UpdateBackground(currentNode.background);
        UpdateCharacter(currentNode.speaker, currentNode.characterEmotion);
        PlayBGM(currentNode.bgm);
        PlayVoice(currentNode.voice);
        PlaySoundEffect(currentNode.soundEffect);

        for (int i = 0; i < choiceButtons.Length; i++)
        {
            if (i < currentNode.choices.Count)
            {
                choiceButtons[i].gameObject.SetActive(true);
                choiceButtons[i].GetComponentInChildren<Text>().text = currentNode.choices[i].text;
                int nextId = currentNode.choices[i].nextId;
                choiceButtons[i].onClick.RemoveAllListeners();
                choiceButtons[i].onClick.AddListener(() => SetDialogue(nextId));
            }
            else
            {
                choiceButtons[i].gameObject.SetActive(false);
            }
        }
    }

    private async void UpdateBackground(string backgroundName)
    {
        if (!string.IsNullOrEmpty(backgroundName))
        {
            var handle = Addressables.LoadAssetAsync<Sprite>($"Backgrounds/{backgroundName}");
            await handle.Task;
            if (handle.Status == AsyncOperationStatus.Succeeded)
            {
                background.sprite = handle.Result;
            }
        }
    }

    private async void UpdateCharacter(string characterName, string emotion)
    {
        if (!string.IsNullOrEmpty(characterName))
        {
            characterImage.gameObject.SetActive(true);
            var handle = Addressables.LoadAssetAsync<Sprite>($"Characters/{characterName}_{emotion}");
            await handle.Task;
            if (handle.Status == AsyncOperationStatus.Succeeded)
            {
                characterImage.sprite = handle.Result;
            }
        }
        else
        {
            characterImage.gameObject.SetActive(false);
        }
    }

    private async void PlayBGM(string bgmName)
    {
        if (!string.IsNullOrEmpty(bgmName))
        {
            var handle = Addressables.LoadAssetAsync<AudioClip>($"BGM/{bgmName}");
            await handle.Task;
            if (handle.Status == AsyncOperationStatus.Succeeded)
            {
                bgmSource.clip = handle.Result;


 bgmSource.Play();
            }
        }
    }

    private async void PlayVoice(string voiceName)
    {
        if (!string.IsNullOrEmpty(voiceName))
        {
            var handle = Addressables.LoadAssetAsync<AudioClip>($"Voices/{voiceName}");
            await handle.Task;
            if (handle.Status == AsyncOperationStatus.Succeeded)
            {
                voiceSource.clip = handle.Result;
                voiceSource.Play();
            }
        }
    }

    private async void PlaySoundEffect(string soundEffectName)
    {
        if (!string.IsNullOrEmpty(soundEffectName))
        {
            var handle = Addressables.LoadAssetAsync<AudioClip>($"Effects/{soundEffectName}");
            await handle.Task;
            if (handle.Status == AsyncOperationStatus.Succeeded)
            {
                soundEffectSource.PlayOneShot(handle.Result);
            }
        }
    }
}

5. テストとデバッグ

  1. CSVファイルを更新し、シーンやサウンドの情報を調整します。
  2. Unityエディタでプレイして、背景やキャラクターの表情、サウンドが正しく再生されるか確認します。

6. まとめ

この資料では、Unityのテキストアドベンチャーゲームにおける、背景やキャラクター、サウンドをAddressablesで管理する方法を紹介しました。この構成により、柔軟なストーリー構築と効率的なリソース管理が可能です。

Unity

Posted by hidepon