技術資料:TextMesh Pro を使ったテキストアドベンチャーの作成方法

目次

クラスを使わない方法

この資料は、Unityを使用してシンプルなテキストアドベンチャーゲームを作成するためのガイドです。特に、TextMesh Proを活用してテキストを美しく表示し、ゲームのストーリー要素を強調する方法に焦点を当てています。TextMesh Proのインポートから、テキスト表示の設定、UI要素の作成まで、基本的なステップを詳しく解説しています。クラスを使わない方法を学びたい方や、シンプルなテキスト表示に興味がある方にも最適な資料です。

概要

TextMesh Proは、テキストの表示を柔軟にカスタマイズし、美しく表示するためのツールです。この資料では、UnityにTextMesh Proをインポートして、簡単なテキストアドベンチャーを作成する手順を解説します。ゲームのストーリー表示に最適なTextMesh Proの基本的な使用方法を紹介します。


1. TextMesh Proのインポート

  1. Unityのメニューバーから「Window > Package Manager」を開きます。
  2. 「TextMesh Pro」を検索してインポートします。
  3. インポート後に「Import TMP Essentials」を選択し、必要なリソースをインポートします。

日本語の表示をしたい場合

JIS第一水準を選択して、ほとんどの日本語に対応できるようにしましょう
登録されていない文字が出てきた時でも、都度追加することで対応できます

必要な漢字を都度追加

2. 必要なUI要素の作成

  • Canvasを作成し、テキストとボタンを配置します。
    • TextMesh Pro – Text (UI) オブジェクトをCanvas上に追加し、「StoryText」に名前を変更します。これはストーリーを表示するテキスト領域です。
    • Buttonを2つ追加し、それぞれ「ChoiceButton1」「ChoiceButton2」と名前を付けます。

3. スクリプトの作成

TextAdventureController.cs

  1. 新規に「TextAdventureController.cs」スクリプトを作成し、Canvasにアタッチします。
  2. 以下のコードをスクリプトに追加します。
   using UnityEngine;
   using TMPro;
   using UnityEngine.UI;

   public class TextAdventureController : MonoBehaviour
   {
       public TextMeshProUGUI storyText; // TextMesh Pro 用のテキスト参照
       public Button choiceButton1;
       public Button choiceButton2;

       private int storyProgress = 0;

       void Start()
       {
           UpdateStory();
           choiceButton1.onClick.AddListener(() => OnChoiceMade(1));
           choiceButton2.onClick.AddListener(() => OnChoiceMade(2));
       }

       void UpdateStory()
       {
           switch (storyProgress)
           {
               case 0:
                   storyText.text = "あなたは暗い森に立っています。前方には道が2つに分かれています。";
                   choiceButton1.GetComponentInChildren<TextMeshProUGUI>().text = "左の道を進む";
                   choiceButton2.GetComponentInChildren<TextMeshProUGUI>().text = "右の道を進む";
                   break;
               case 1:
                   storyText.text = "左の道を進んだあなたは、小川にたどり着きました。";
                   choiceButton1.GetComponentInChildren<TextMeshProUGUI>().text = "川を渡る";
                   choiceButton2.GetComponentInChildren<TextMeshProUGUI>().text = "戻る";
                   break;
               case 2:
                   storyText.text = "右の道を進んだあなたは、廃墟にたどり着きました。";
                   choiceButton1.GetComponentInChildren<TextMeshProUGUI>().text = "廃墟を探索する";
                   choiceButton2.GetComponentInChildren<TextMeshProUGUI>().text = "戻る";
                   break;
               default:
                   storyText.text = "冒険はここで終わりです。";
                   choiceButton1.gameObject.SetActive(false);
                   choiceButton2.gameObject.SetActive(false);
                   break;
           }
       }

       void OnChoiceMade(int choice)
       {
           if (storyProgress == 0 && choice == 1) storyProgress = 1;
           else if (storyProgress == 0 && choice == 2) storyProgress = 2;
           else if (storyProgress == 1 && choice == 2) storyProgress = 0;
           else if (storyProgress == 2 && choice == 2) storyProgress = 0;
           UpdateStory();
       }
   }

4. 各パーツの設定

  • スクリプトの TextMeshProUGUIButton オブジェクトに、Canvas上の各UI要素をドラッグしてアタッチします。
  • UpdateStory関数で、ストーリーの進行に応じてテキストと選択肢の内容が変わるように設定します。

5. 実行とテスト

  • ゲームを再生し、ボタンをクリックして選択肢を選ぶことで、テキストアドベンチャーが進行することを確認します。

6. 応用とカスタマイズ

  • TextMesh Proのフォントサイズやスタイルを調整して、テキスト表示の見た目を変更できます。
  • テキストにシャドウやアウトラインのエフェクトを追加して、視覚効果を強化します。

この資料を通じて、TextMesh Proを使ってテキスト表示をカスタマイズし、魅力的なテキストアドベンチャーを作成する方法が学べます。

クラスを使う方法

クラスを使ってテキストアドベンチャーのストーリーや分岐を管理することで、コードが整理され、柔軟な構造を持つことができます。以下は、クラスを使ってストーリーの分岐を管理する方法を示したチュートリアルです。

1. ストーリーのデータクラスを作成する

まず、各ストーリーのシーンを管理するためのデータクラスを作成します。ここでは、StoryNodeというクラスを使って、シーンのテキストと選択肢を定義します。

public class StoryNode
{
    public string StoryText; // ストーリーのテキスト
    public string Choice1Text; // 選択肢1のテキスト
    public string Choice2Text; // 選択肢2のテキスト
    public int Choice1NextNode; // 選択肢1を選んだときの次のノード番号
    public int Choice2NextNode; // 選択肢2を選んだときの次のノード番号

    public StoryNode(string storyText, string choice1Text, int choice1NextNode, string choice2Text, int choice2NextNode)
    {
        StoryText = storyText;
        Choice1Text = choice1Text;
        Choice1NextNode = choice1NextNode;
        Choice2Text = choice2Text;
        Choice2NextNode = choice2NextNode;
    }
}

2. ストーリーコントローラークラスの作成

次に、TextAdventureControllerクラスで、ストーリーの進行を管理します。このクラスでStoryNodeのインスタンスを使い、選択に応じて次のシーンを表示します。

using UnityEngine;
using TMPro;
using UnityEngine.UI;
using System.Collections.Generic;

public class TextAdventureController : MonoBehaviour
{
    public TextMeshProUGUI storyText;
    public Button choiceButton1;
    public Button choiceButton2;

    private Dictionary<int, StoryNode> storyNodes = new Dictionary<int, StoryNode>();
    private int currentNode = 0;

    void Start()
    {
        InitializeStoryNodes();
        UpdateStory();

        choiceButton1.onClick.AddListener(() => OnChoiceMade(1));
        choiceButton2.onClick.AddListener(() => OnChoiceMade(2));
    }

    void InitializeStoryNodes()
    {
        // ストーリーの各ノードを設定します
        storyNodes[0] = new StoryNode("あなたは暗い森に立っています。前方には道が2つに分かれています。",
                                      "左の道を進む", 1,
                                      "右の道を進む", 2);
        storyNodes[1] = new StoryNode("左の道を進んだあなたは、小川にたどり着きました。",
                                      "川を渡る", 3,
                                      "戻る", 0);
        storyNodes[2] = new StoryNode("右の道を進んだあなたは、廃墟にたどり着きました。",
                                      "廃墟を探索する", 4,
                                      "戻る", 0);
        storyNodes[3] = new StoryNode("あなたは川を渡り、森の奥へと進んでいきます。冒険の終わりが近づいている...",
                                      "続ける", -1,
                                      "戻る", 0);
        storyNodes[4] = new StoryNode("廃墟の中で不思議なアイテムを見つけました。",
                                      "アイテムを調べる", -1,
                                      "戻る", 0);
    }

    void UpdateStory()
    {
        if (currentNode == -1)
        {
            storyText.text = "冒険はここで終わりです。";
            choiceButton1.gameObject.SetActive(false);
            choiceButton2.gameObject.SetActive(false);
        }
        else
        {
            var node = storyNodes[currentNode];
            storyText.text = node.StoryText;
            choiceButton1.GetComponentInChildren<TextMeshProUGUI>().text = node.Choice1Text;
            choiceButton2.GetComponentInChildren<TextMeshProUGUI>().text = node.Choice2Text;
            choiceButton1.gameObject.SetActive(true);
            choiceButton2.gameObject.SetActive(true);
        }
    }

    void OnChoiceMade(int choice)
    {
        if (currentNode != -1)
        {
            var node = storyNodes[currentNode];
            currentNode = (choice == 1) ? node.Choice1NextNode : node.Choice2NextNode;
            UpdateStory();
        }
    }
}

3. 各パーツの設定

  • スクリプト内の TextMeshProUGUIButton オブジェクトに、Canvas上の各UI要素をドラッグしてアタッチします。

4. 実行とテスト

  • ゲームを再生し、選択肢をクリックしてテキストアドベンチャーが進行することを確認します。

この構造の利点

  • 各ストーリーシーンが StoryNode クラスとして独立しているため、ストーリーの追加や変更が容易です。
  • 分岐が多くなるときも、データ構造が整っているため管理がしやすく、柔軟に拡張できます。

これで、クラスを使ったテキストアドベンチャーのチュートリアルが完成です。

データの管理をインスペクターで行う

Unityでデータを[System.Serializable]にして、インスペクターから直接編集できるようにするのはとても良い方法です。これにより、プログラムを変更せずに、インスペクター上で簡単にシーンデータや選択肢を調整できるようになります。

以下のように、StoryNodeをシリアライズ可能なクラスとして定義し、TextAdventureControllerクラス内でリストとして扱うことで、Unityのインスペクターから直接編集が可能になります。

1. ストーリーのデータクラスをシリアライズ可能にする

まず、StoryNodeクラスをシリアライズ可能にし、インスペクターから設定できるようにします。

using UnityEngine;

[System.Serializable]
public class StoryNode
{
    [TextArea] public string storyText; // ストーリーのテキストをインスペクターで編集可能に
    public string choice1Text; // 選択肢1のテキスト
    public string choice2Text; // 選択肢2のテキスト
    public int choice1NextNode; // 選択肢1を選んだときの次のノード番号
    public int choice2NextNode; // 選択肢2を選んだときの次のノード番号
}

2. TextAdventureControllerでストーリーノードをリストとして保持する

次に、TextAdventureControllerクラスで、StoryNodeのリストを作成し、インスペクターから編集できるようにします。

using UnityEngine;
using TMPro;
using UnityEngine.UI;
using System.Collections.Generic;

public class TextAdventureController : MonoBehaviour
{
    public TextMeshProUGUI storyText; // ストーリー表示用のテキスト
    public Button choiceButton1;
    public Button choiceButton2;

    public List<StoryNode> storyNodes = new List<StoryNode>(); // インスペクターから編集可能なストーリーノードのリスト
    private int currentNode = 0; // 現在のノード番号

    void Start()
    {
        UpdateStory();

        // 選択肢ボタンにリスナーを追加
        choiceButton1.onClick.AddListener(() => OnChoiceMade(1));
        choiceButton2.onClick.AddListener(() => OnChoiceMade(2));
    }

    void UpdateStory()
    {
        if (currentNode == -1 || currentNode >= storyNodes.Count)
        {
            storyText.text = "冒険はここで終わりです。";
            choiceButton1.gameObject.SetActive(false);
            choiceButton2.gameObject.SetActive(false);
        }
        else
        {
            // 現在のノードを取得し、テキストとボタンに反映
            var node = storyNodes[currentNode];
            storyText.text = node.storyText;
            choiceButton1.GetComponentInChildren<TextMeshProUGUI>().text = node.choice1Text;
            choiceButton2.GetComponentInChildren<TextMeshProUGUI>().text = node.choice2Text;
            choiceButton1.gameObject.SetActive(true);
            choiceButton2.gameObject.SetActive(true);
        }
    }

    void OnChoiceMade(int choice)
    {
        // 現在のノードに基づき、次のノードに進む
        if (currentNode != -1 && currentNode < storyNodes.Count)
        {
            var node = storyNodes[currentNode];
            currentNode = (choice == 1) ? node.choice1NextNode : node.choice2NextNode;
            UpdateStory();
        }
    }
}

3. インスペクターでストーリーを設定する

  1. Unityエディターで、TextAdventureControllerをアタッチしているGameObject(Canvasなど)を選択します。
  2. TextAdventureControllerのインスペクターに表示されているstoryNodesリストに必要な要素数を指定し、それぞれのStoryNodeに以下の情報を入力します。
  • storyText: 各シーンのテキスト。
  • choice1Textおよびchoice2Text: 選択肢のテキスト。
  • choice1NextNodeおよびchoice2NextNode: 次のシーンのノード番号(-1にすると、ゲーム終了とするなどの分岐を表現できます)。

4. 実行とテスト

  • ゲームを再生し、インスペクターで設定したストーリーが表示されること、選択肢が反映されて正しく分岐することを確認します。

このアプローチのメリット

  • インスペクターでデータを編集できるため、プログラミングの知識が少ない人でも簡単にストーリーを変更・追加できます。
  • ストーリーの内容をコードに直接書く必要がないため、データの管理がしやすくなります。

この方法で、インスペクターから簡単にテキストアドベンチャーの内容をカスタマイズできるようになります。

データをScriptableObjectで管理する

ScriptableObjectを使うと、Unityエディター上でストーリーの内容を直感的に管理でき、スクリプトコードからストーリー内容を分離できるため、メンテナンスが楽になります。以下は、ScriptableObjectを使ってテキストアドベンチャーを構築するチュートリアルです。

1. ScriptableObjectの作成

まず、ストーリーの各ノードを管理するためのScriptableObjectを作成します。

1.1 StoryNode ScriptableObjectクラスの作成

  1. Scriptsフォルダで「StoryNode」というC#スクリプトを作成します。
  2. 以下のコードを追加して、各ストーリーノードを管理するためのクラスを定義します。
   using UnityEngine;

   [CreateAssetMenu(fileName = "NewStoryNode", menuName = "TextAdventure/StoryNode")]
   public class StoryNode : ScriptableObject
   {
       [TextArea(3, 10)]
       public string storyText; // ストーリーのテキスト
       public string choice1Text; // 選択肢1のテキスト
       public string choice2Text; // 選択肢2のテキスト
       public StoryNode choice1NextNode; // 選択肢1を選んだときの次のノード
       public StoryNode choice2NextNode; // 選択肢2を選んだときの次のノード
   }

1.2 ストーリーノードのインスタンスを作成

  1. 「Assets」フォルダ内に「StoryNodes」という新しいフォルダを作成します。
  2. 右クリックして「Create > TextAdventure > StoryNode」を選択し、ストーリーノードを作成します。
  3. 必要なだけノードを作成し、各ノードのstoryTextchoice1Textchoice2Text、およびそれぞれの選択肢が指す次のノードを設定します。 例えば:
  • Node0: 「暗い森に立っています。」「左の道を進む」「右の道を進む」など。
  • Node1: 「小川にたどり着きました。」「川を渡る」「戻る」など。

2. ストーリーコントローラーのスクリプト作成

次に、ScriptableObjectを使ってストーリーを進行するTextAdventureControllerスクリプトを作成します。

using UnityEngine;
using TMPro;
using UnityEngine.UI;

public class TextAdventureController : MonoBehaviour
{
    public TextMeshProUGUI storyText;
    public Button choiceButton1;
    public Button choiceButton2;
    public StoryNode startingNode; // 開始ノードを設定するためのフィールド

    private StoryNode currentNode;

    void Start()
    {
        currentNode = startingNode;
        UpdateStory();

        choiceButton1.onClick.AddListener(() => OnChoiceMade(1));
        choiceButton2.onClick.AddListener(() => OnChoiceMade(2));
    }

    void UpdateStory()
    {
        if (currentNode != null)
        {
            storyText.text = currentNode.storyText;
            choiceButton1.GetComponentInChildren<TextMeshProUGUI>().text = currentNode.choice1Text;
            choiceButton2.GetComponentInChildren<TextMeshProUGUI>().text = currentNode.choice2Text;
            choiceButton1.gameObject.SetActive(true);
            choiceButton2.gameObject.SetActive(true);
        }
        else
        {
            storyText.text = "冒険はここで終わりです。";
            choiceButton1.gameObject.SetActive(false);
            choiceButton2.gameObject.SetActive(false);
        }
    }

    void OnChoiceMade(int choice)
    {
        currentNode = (choice == 1) ? currentNode.choice1NextNode : currentNode.choice2NextNode;
        UpdateStory();
    }
}

3. 各パーツの設定

  • TextAdventureControllerスクリプトのstartingNodeフィールドに、開始となるStoryNode(例えば、Node0)をドラッグ&ドロップして設定します。
  • TextMeshProUGUIButtonオブジェクトも同様に設定します。

4. 実行とテスト

  • ゲームを再生し、ボタンをクリックしてテキストアドベンチャーが進行することを確認します。

ScriptableObjectを使うメリット

  • ストーリーデータがエディター上で直感的に編集可能:ノードのテキストや次のノードを視覚的に設定できます。
  • コードとデータの分離:ストーリー内容をScriptableObjectに分離することで、コードがシンプルで保守しやすくなります。
  • 拡張性:新しいシーンを追加する場合、ScriptableObjectのインスタンスを増やすだけで、スクリプトの修正なしに新しいストーリー分岐を追加できます。

ScriptableObjectを使用することで、複雑なテキストアドベンチャーも効率的に管理できます。

データをエクセルで管理する方法

エクセルファイルからストーリーデータを読み取ると、Unityエディターを使わずにデータを一括編集でき、Excelを使ったデータ管理が容易になります。Unityでエクセルファイルを読み取るには、CSVファイルとしてエクスポートし、それを読み込む方法が一般的です。CSVファイルはテキスト形式で扱いやすく、Unityのコードから簡単に読み取れます。

以下は、CSVファイルを使ってテキストアドベンチャーのストーリーデータを読み込む方法です。

1. エクセルデータの準備

  1. Excelで以下のような形式でストーリーデータを作成します。
  2. データを「CSV (カンマ区切り)」形式でエクスポートし、Unityプロジェクトの「Assets」フォルダ内に保存します(例: story.csv)。
NodeIDStoryTextChoice1TextChoice1NextIDChoice2TextChoice2NextID
0あなたは暗い森に立っています。左の道を進む1右の道を進む2
1左の道を進んだあなたは小川に着きました。川を渡る3戻る0
2右の道を進んだあなたは廃墟に着きました。廃墟を探索する4戻る0
3あなたは川を渡り、冒険を続けます。続ける-1戻る0
4廃墟で不思議なアイテムを見つけました。アイテムを調べる-1戻る0

2. CSVファイルを読み取るスクリプトの作成

CSVファイルを読み取るためのスクリプトを作成し、ストーリーデータを動的に読み込みます。

2.1 StoryNodeクラスの定義

まず、CSVから読み取ったデータを保持するためのStoryNodeクラスを定義します。

public class StoryNode
{
    public int NodeID;
    public string StoryText;
    public string Choice1Text;
    public int Choice1NextID;
    public string Choice2Text;
    public int Choice2NextID;

    public StoryNode(int nodeID, string storyText, string choice1Text, int choice1NextID, string choice2Text, int choice2NextID)
    {
        NodeID = nodeID;
        StoryText = storyText;
        Choice1Text = choice1Text;
        Choice1NextID = choice1NextID;
        Choice2Text = choice2Text;
        Choice2NextID = choice2NextID;
    }
}

2.2 CSVファイルを読み取るスクリプト

次に、TextAdventureControllerスクリプトにCSVを読み込む機能を追加します。

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

public class TextAdventureController : MonoBehaviour
{
    public TextMeshProUGUI storyText;
    public Button choiceButton1;
    public Button choiceButton2;

    private Dictionary<int, StoryNode> storyNodes = new Dictionary<int, StoryNode>();
    private int currentNodeID = 0;

    void Start()
    {
        LoadStoryFromCSV("Assets/story.csv");
        UpdateStory();

        choiceButton1.onClick.AddListener(() => OnChoiceMade(1));
        choiceButton2.onClick.AddListener(() => OnChoiceMade(2));
    }

    void LoadStoryFromCSV(string filePath)
    {
        if (!File.Exists(filePath))
        {
            Debug.LogError("CSVファイルが見つかりません: " + filePath);
            return;
        }

        using (var reader = new StreamReader(filePath))
        {
            bool isHeader = true;
            while (!reader.EndOfStream)
            {
                var line = reader.ReadLine();
                if (isHeader) // ヘッダー行をスキップ
                {
                    isHeader = false;
                    continue;
                }

                var values = line.Split(',');
                int nodeID = int.Parse(values[0]);
                string storyText = values[1];
                string choice1Text = values[2];
                int choice1NextID = int.Parse(values[3]);
                string choice2Text = values[4];
                int choice2NextID = int.Parse(values[5]);

                var node = new StoryNode(nodeID, storyText, choice1Text, choice1NextID, choice2Text, choice2NextID);
                storyNodes[nodeID] = node;
            }
        }
    }

    void UpdateStory()
    {
        if (!storyNodes.ContainsKey(currentNodeID))
        {
            storyText.text = "冒険はここで終わりです。";
            choiceButton1.gameObject.SetActive(false);
            choiceButton2.gameObject.SetActive(false);
            return;
        }

        var node = storyNodes[currentNodeID];
        storyText.text = node.StoryText;
        choiceButton1.GetComponentInChildren<TextMeshProUGUI>().text = node.Choice1Text;
        choiceButton2.GetComponentInChildren<TextMeshProUGUI>().text = node.Choice2Text;
        choiceButton1.gameObject.SetActive(true);
        choiceButton2.gameObject.SetActive(true);
    }

    void OnChoiceMade(int choice)
    {
        if (storyNodes.ContainsKey(currentNodeID))
        {
            var node = storyNodes[currentNodeID];
            currentNodeID = (choice == 1) ? node.Choice1NextID : node.Choice2NextID;
            UpdateStory();
        }
    }
}

3. 実行とテスト

  1. スクリプトをUnityエディター上のCanvasにアタッチします。
  2. ゲームを実行し、ストーリーがCSVファイルから正しく読み込まれ、選択肢に応じて次のシーンが進行するかを確認します。

この方法のメリット

  • データ管理が容易:Excelでデータを一括管理し、変更や追加も簡単です。
  • コードからのデータ分離:CSVファイルでデータを管理することで、コードが整理され、再利用や拡張が容易になります。
  • 拡張性:大量のストーリーノードを持つ場合でも、CSVファイルで簡単に対応可能です。

エクセルを使ったデータ管理は、テキストアドベンチャーのような構造に特に有効で、スクリプトの柔軟性を保ちながらデータ編集が簡単に行えます。

ScriptableObjectとエクセルのデータ管理の比較

ScriptableObjectとCSVファイルの両方には、それぞれメリットとデメリットがあり、場合によっては両方を組み合わせて採用することもできます。以下は、ScriptableObjectとCSVの特徴を比較し、どのような場合にどちらを採用するか、または両方を採用する方法について説明します。

ScriptableObjectの特徴

メリット

  1. エディター内での直感的な編集:Unityエディターで直接ストーリーデータを作成・編集できるため、使い勝手が良いです。
  2. データの型安全性:ScriptableObjectのプロパティは型が定義されているため、データの不整合やエラーが発生しにくく、タイプミスやフォーマットのエラーが防げます。
  3. 柔軟な設定:オブジェクト間の参照(例えば次のノード)もエディター上で直感的に設定できます。
  4. プロジェクト管理の一元化:ScriptableObjectはUnityのアセットとして保存されるため、Unity内でのバージョン管理や整理が容易です。

デメリット

  1. エディター依存:ScriptableObjectはUnityエディター内でしか編集できないため、データの一括編集や他のツール(Excelなど)での管理が難しいです。
  2. データ量が多いと管理が複雑に:大量のノードやデータを持つ場合、個々のScriptableObjectファイルを作成するのは煩雑です。

CSVファイルの特徴

メリット

  1. 外部ツールでのデータ管理:Excelなどでデータを一括管理・編集できるため、大量のデータを扱いやすくなります。
  2. 外部とのデータ連携がしやすい:CSV形式はテキストファイルとして保存できるため、他のアプリケーションと簡単に連携が可能です。
  3. 柔軟なデータ更新:データの変更や追加が容易で、エディターを起動せずに内容を編集できるため、素早く反映が可能です。

デメリット

  1. データの型が不安定:データの形式がテキストであるため、タイプミスや不整合(例えば、誤ったID参照)などが発生しやすいです。
  2. エディター内でのプレビューが難しい:Unityエディターで直接CSVファイルを確認・編集できないため、データの変更が適切に反映されているかは実行しないと確認しづらいです。

両方を組み合わせて使用する方法

ScriptableObjectとCSVの両方を組み合わせて使用することで、データ管理と安全性の両方のメリットを享受することが可能です。

1. データのベースはCSVで管理し、インポートしてScriptableObjectに反映

  • 外部でCSVファイルを編集し、それをUnityにインポートしてScriptableObjectに変換する仕組みを作ります。この方法では、外部ツールで大規模なデータ編集が可能になりつつ、Unity内でScriptableObjectとしてデータを扱えるので、型安全性が保たれます。
  • 例えば、StoryNode ScriptableObjectを用意しておき、CSVImporterスクリプトを作成して、CSVファイルを読み込み、ScriptableObjectインスタンスにデータを反映するようにします。

2. ランタイムでCSVを読み込み、ScriptableObjectでリファレンス管理

  • データは外部のCSVファイルで管理しつつ、Unityエディター上で重要なデータ(開始ノードや主要ノードなど)をScriptableObjectで定義しておきます。CSVから読み込んだデータは、ScriptableObjectを参照しながらランタイムで使用することで、エディターとランタイムの双方での一貫性を確保します。

実装例

例1: CSVからScriptableObjectを生成するインポート機能

  1. CSVインポートスクリプトを作成して、CSVをScriptableObjectに変換します。
using UnityEngine;

[CreateAssetMenu(fileName = "NewStoryNode", menuName = "TextAdventure/StoryNode")]
public class StoryNode : ScriptableObject
{
    public int NodeID;
    public string StoryText;
    public string Choice1Text;
    public string Choice2Text;

    public int Choice1NextID; // 選択肢1を選んだときの次のノード
    public int Choice2NextID; // 選択肢2を選んだときの次のノード


    public StoryNode(int nodeID, string storyText, string choice1Text, int choice1NextID, string choice2Text, int choice2NextID)
    {
        NodeID = nodeID;
        StoryText = storyText;
        Choice1Text = choice1Text;
        Choice1NextID = choice1NextID;
        Choice2Text = choice2Text;
        Choice2NextID = choice2NextID;
    }
}
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections.Generic;

public class CSVImporter : MonoBehaviour
{
    [MenuItem("Tools/Import Story from CSV")]
    public static void ImportStoryFromCSV()
    {
        string filePath = "Assets/story.csv";

        if (!File.Exists(filePath))
        {
            Debug.LogError("CSVファイルが見つかりません: " + filePath);
            return;
        }

        var nodes = new Dictionary<int, StoryNode>(); // ノードIDをキーにしてノードを格納
        var nodeDataList = new List<string[]>(); // 読み込んだデータ行を一時的に格納

        // 1. CSVを読み込み、StoryNodeインスタンスを作成
        using (var reader = new StreamReader(filePath))
        {
            bool isHeader = true;
            while (!reader.EndOfStream)
            {
                var line = reader.ReadLine();
                if (isHeader)
                {
                    isHeader = false;
                    continue;
                }

                var values = line.Split(',');
                nodeDataList.Add(values); // データ行を保存

                int nodeID = int.Parse(values[0]);
                string storyText = values[1];
                string choice1Text = values[2];
                int choice1NextID = int.Parse(values[3]);
                string choice2Text = values[4];
                int choice2NextID = int.Parse(values[5]);

                // StoryNodeのインスタンスを作成し、基本情報を設定
                StoryNode node = ScriptableObject.CreateInstance<StoryNode>();
                node.StoryText = storyText;
                node.Choice1Text = choice1Text;
                node.Choice2Text = choice2Text;
                node.Choice1NextID = choice1NextID;
                node.Choice2NextID = choice2NextID;

                // ノードを一時的に辞書に格納してIDで管理
                nodes[nodeID] = node;

                // アセットとして保存
                AssetDatabase.CreateAsset(node, $"Assets/StoryNodes/Node_{nodeID}.asset");
            }
        }

        // 2. 次のノードの参照を設定
        foreach (var values in nodeDataList)
        {
            int nodeID = int.Parse(values[0]);
            int choice1NextID = int.Parse(values[3]);
            int choice2NextID = int.Parse(values[5]);

            StoryNode node = nodes[nodeID];

            // 選択肢に基づいて次のノードを設定
            if (nodes.ContainsKey(choice1NextID))
            {
                node.Choice1NextID = choice1NextID;
            }

            if (nodes.ContainsKey(choice2NextID))
            {
                node.Choice2NextID = choice2NextID;
            }
        }

        AssetDatabase.SaveAssets();
        Debug.Log("CSVからのインポートが完了しました!");
    }
}

上部メニューに「Tools>Import Story from CSV」メニューが作成されていますので、ここから作成できます

「Assets/Story Nodes」フォルダにScriptableObjectとして自動的に作成されます

  1. インポート後、エディターでScriptableObjectを編集し、参照を調整できます。

例2: CSVをランタイムで読み込み、ScriptableObjectの参照を利用

  1. 主要なノード(開始ノードなど)はScriptableObjectで保持し、他のノードはCSVから動的に読み込んで使用します。
  2. TextAdventureControllerがScriptableObjectを参照しつつ、ランタイムで読み込んだデータを使用してストーリーを進行させます。

このように、外部でのデータ編集のしやすさと、Unityエディターでの管理の簡便さを両立させることが可能です。

データ管理にAddressableを使う

Addressableを組み合わせると、データを柔軟に管理しつつ、ゲームのパフォーマンスを向上させることができます。特に、Addressableを使うことで、ScriptableObjectやCSVから読み込んだデータを必要なタイミングで非同期にロードできるため、大規模なデータやリソースを扱う場合に最適です。

以下に、AddressableとScriptableObjectやCSVファイルの組み合わせについてのメリットと導入手順を説明します。

Addressableの利点

  1. メモリ管理の向上:大きなテキストアドベンチャーや膨大なストーリーデータでも、必要なときにだけデータをロードできるため、メモリ消費を抑えられます。
  2. データの動的ロード:アドベンチャーの進行に合わせて、次のシーンやノードをロードすることで、無駄なデータの読み込みを減らし、ゲームのパフォーマンスを改善できます。
  3. コンテンツの更新が容易:Addressableで設定したデータは、リモートから更新可能です。これにより、アドベンチャーの内容をアプリの更新なしで追加・修正できます。

Addressableの導入手順

1. Addressableパッケージのインストール

  1. Package Managerから「Addressable Asset System」パッケージをインストールします。
  2. インストール後、「Window > Asset Management > Addressables > Groups」から「Addressables Groups」ウィンドウを開きます。

2. ScriptableObjectのAddressable設定

  1. StoryNode ScriptableObjectを各ノードごとに作成し、Addressableとして登録します。
  2. 各アセットのインスペクタで「Addressable」にチェックを入れて、ユニークな名前(例:Node_0Node_1)を設定します。これにより、ノードの読み込みが名前で行えるようになります。

3. CSVからScriptableObjectを生成し、Addressableとして登録

CSVから読み込んだデータをもとに、動的にScriptableObjectを生成してAddressableに登録する方法です。

using UnityEngine;
using UnityEditor;
using System.IO;
using UnityEngine.AddressableAssets;

public class CSVImporter : MonoBehaviour
{
    [MenuItem("Tools/Import Story from CSV")]
    public static void ImportStoryFromCSV()
    {
        string filePath = "Assets/story.csv";
        using (var reader = new StreamReader(filePath))
        {
            bool isHeader = true;
            while (!reader.EndOfStream)
            {
                var line = reader.ReadLine();
                if (isHeader)
                {
                    isHeader = false;
                    continue;
                }

                var values = line.Split(',');
                StoryNode node = ScriptableObject.CreateInstance<StoryNode>();
                node.storyText = values[1];
                node.choice1Text = values[2];
                node.choice1NextNode = /* 次のノードID */;
                node.choice2Text = values[4];
                node.choice2NextNode = /* 次のノードID */;

                string assetPath = $"Assets/StoryNodes/Node_{values[0]}.asset";
                AssetDatabase.CreateAsset(node, assetPath);

                // Addressableに登録
                AddressableAssetSettingsDefaultObject.Settings.AddAsset(assetPath, node);
            }
            AssetDatabase.SaveAssets();
        }
    }
}

4. ストーリーコントローラーの非同期ロード実装

Addressableを使って非同期にロードすることで、必要なデータをタイミングに合わせて呼び出せます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class TextAdventureController : MonoBehaviour
{
    public TextMeshProUGUI storyText;
    public Button choiceButton1;
    public Button choiceButton2;

    private StoryNode currentNode;

    void Start()
    {
        LoadStoryNode("Node_0"); // 開始ノードをロード
        choiceButton1.onClick.AddListener(() => OnChoiceMade(1));
        choiceButton2.onClick.AddListener(() => OnChoiceMade(2));
    }

    void LoadStoryNode(string nodeID)
    {
        Addressables.LoadAssetAsync<StoryNode>(nodeID).Completed += OnNodeLoaded;
    }

    void OnNodeLoaded(AsyncOperationHandle<StoryNode> obj)
    {
        if (obj.Status == AsyncOperationStatus.Succeeded)
        {
            currentNode = obj.Result;
            UpdateStory();
        }
        else
        {
            Debug.LogError("ノードの読み込みに失敗しました: " + obj.OperationException);
        }
    }

    void UpdateStory()
    {
        if (currentNode == null)
        {
            storyText.text = "冒険はここで終わりです。";
            choiceButton1.gameObject.SetActive(false);
            choiceButton2.gameObject.SetActive(false);
            return;
        }

        storyText.text = currentNode.storyText;
        choiceButton1.GetComponentInChildren<TextMeshProUGUI>().text = currentNode.choice1Text;
        choiceButton2.GetComponentInChildren<TextMeshProUGUI>().text = currentNode.choice2Text;
        choiceButton1.gameObject.SetActive(true);
        choiceButton2.gameObject.SetActive(true);
    }

    void OnChoiceMade(int choice)
    {
        if (currentNode != null)
        {
            string nextNodeID = (choice == 1) ? currentNode.choice1NextNode : currentNode.choice2NextNode;
            Addressables.Release(currentNode); // メモリ解放
            LoadStoryNode(nextNodeID);
        }
    }
}

AddressableとScriptableObjectを組み合わせる利点

  1. メモリの最適化:使用したノードをAddressable経由で解放し、必要なときだけ読み込むため、大量のデータを扱うアドベンチャーでもメモリ効率が良くなります。
  2. 非同期ロードによるUX向上:プレイヤーが選択肢を選んだ後に次のノードを非同期でロードすることで、リソースの切り替えがシームレスに行われます。
  3. 拡張性:Addressableを使えば、新しいノードやシーンを後から簡単に追加でき、リモート更新も可能になります。

AddressableとCSVの併用も検討

さらに、CSVを用いてデータをインポートし、各ストーリーノードをAddressableとして管理することで、エディター上のインポートや修正が楽になり、ゲームをビルド後でもリモートでデータの更新ができるようになります。


Addressableを組み合わせると、ScriptableObjectやCSVの管理が一段と強力になります。特に、非同期ロードやリモート更新が必要なプロジェクトにおいて、Addressableの利点を活かすことで柔軟かつ効率的なデータ管理が可能です。

AddressableとCSVを併用することで、外部で管理・編集しやすいデータの編集と更新の柔軟性と、リソース管理の効率化が両立できます。ここでは、具体的な実装例を交えつつ、AddressableとCSVの併用方法について詳細に説明します。

AddressableとCSVの併用メリット

  1. 柔軟なデータ更新:CSVファイルで外部ツール(Excelなど)を使ってデータを簡単に編集できます。データの変更や追加も容易です。
  2. メモリ管理の最適化:Addressableによって、必要なときにのみデータを非同期でロードし、使用後は解放できるため、大量のデータを効率的に扱えます。
  3. リモートからのデータ更新:AddressableでCSVやScriptableObjectをリモートで管理することで、ゲームの更新なしにデータのみ変更・配信可能です。

実装手順

1. CSVファイルの準備

まず、Excelなどで以下のような形式でデータを作成し、「CSV (カンマ区切り)」形式で保存します。

NodeIDStoryTextChoice1TextChoice1NextIDChoice2TextChoice2NextID
0あなたは暗い森に立っています。左の道を進む1右の道を進む2
1左の道を進んだあなたは小川に着きました。川を渡る3戻る0
2右の道を進んだあなたは廃墟に着きました。廃墟を探索する4戻る0

このCSVファイルもAddressableとして設定し、リモート更新できるようにします。次の手順で設定します。

  1. Assets/Resources/にCSVファイル(例: story.csv)を保存します。
  2. CSVファイルを選択し、インスペクタで「Addressable」にチェックを入れ、名前(例: StoryDataCSV)を設定します。

2. CSVからデータを読み込み、ScriptableObjectに変換するスクリプトの作成

次に、CSVファイルを読み込み、そのデータをもとにストーリーのノードを生成し、各ノードをAddressableとして登録します。以下は、CSVからデータを読み込むスクリプトの例です。

スクリプト: CSVLoader.cs
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class CSVLoader : MonoBehaviour
{
    public Dictionary<int, StoryNode> storyNodes = new Dictionary<int, StoryNode>();

    // Addressableとして指定したCSVファイルをロード
    public void LoadStoryFromCSV()
    {
        Addressables.LoadAssetAsync<TextAsset>("StoryDataCSV").Completed += OnCSVLoaded;
    }

    private void OnCSVLoaded(AsyncOperationHandle<TextAsset> handle)
    {
        if (handle.Status == AsyncOperationStatus.Succeeded)
        {
            TextAsset csvData = handle.Result;
            ParseCSVData(csvData.text);
        }
        else
        {
            Debug.LogError("CSVの読み込みに失敗しました");
        }
    }

    private void ParseCSVData(string csvText)
    {
        using (StringReader reader = new StringReader(csvText))
        {
            bool isHeader = true;
            while (reader.Peek() > -1)
            {
                var line = reader.ReadLine();
                if (isHeader) // ヘッダー行をスキップ
                {
                    isHeader = false;
                    continue;
                }

                var values = line.Split(',');
                int nodeID = int.Parse(values[0]);
                string storyText = values[1];
                string choice1Text = values[2];
                int choice1NextID = int.Parse(values[3]);
                string choice2Text = values[4];
                int choice2NextID = int.Parse(values[5]);

                // CSVから読み込んだデータをもとにStoryNode ScriptableObjectを生成
                StoryNode node = ScriptableObject.CreateInstance<StoryNode>();
                node.storyText = storyText;
                node.choice1Text = choice1Text;
                node.choice1NextID = choice1NextID;
                node.choice2Text = choice2Text;
                node.choice2NextID = choice2NextID;

                // StoryNodeをAddressableに追加
                storyNodes[nodeID] = node;
            }
        }
    }
}

3. Addressableによるデータの非同期読み込み

ストーリーコントローラーで、選択に応じてAddressableから非同期でノードを読み込み、プレイヤーの選択によってストーリーを進行させます。

スクリプト: TextAdventureController.cs
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class TextAdventureController : MonoBehaviour
{
    public TextMeshProUGUI storyText;
    public Button choiceButton1;
    public Button choiceButton2;

    private CSVLoader csvLoader;
    private StoryNode currentNode;

    void Start()
    {
        csvLoader = GetComponent<CSVLoader>();
        csvLoader.LoadStoryFromCSV();

        choiceButton1.onClick.AddListener(() => OnChoiceMade(1));
        choiceButton2.onClick.AddListener(() => OnChoiceMade(2));
    }

    void UpdateStory()
    {
        if (currentNode == null)
        {
            storyText.text = "冒険はここで終わりです。";
            choiceButton1.gameObject.SetActive(false);
            choiceButton2.gameObject.SetActive(false);
            return;
        }

        storyText.text = currentNode.storyText;
        choiceButton1.GetComponentInChildren<TextMeshProUGUI>().text = currentNode.choice1Text;
        choiceButton2.GetComponentInChildren<TextMeshProUGUI>().text = currentNode.choice2Text;
        choiceButton1.gameObject.SetActive(true);
        choiceButton2.gameObject.SetActive(true);
    }

    void OnChoiceMade(int choice)
    {
        int nextNodeID = (choice == 1) ? currentNode.choice1NextID : currentNode.choice2NextID;

        // 次のノードを非同期に読み込み
        if (csvLoader.storyNodes.ContainsKey(nextNodeID))
        {
            currentNode = csvLoader.storyNodes[nextNodeID];
            UpdateStory();
        }
        else
        {
            Debug.LogError("次のノードが存在しません: " + nextNodeID);
        }
    }
}

4. Addressableのリモート設定(オプション)

  1. Addressablesのグループ設定でCSVやScriptableObjectをリモートグループに設定し、リモートサーバーからのロードができるようにします。
  2. CSVデータを更新したい場合、リモートにアップロードしておくことで、ゲームのアップデートを行わずに内容を変更できます。

まとめ

  • CSVファイルをAddressableでロードし、StoryNodeのようなScriptableObjectを動的に生成。
  • 次のノードをAddressable経由で非同期ロードし、選択肢に応じてシームレスにストーリーを進行。
  • リモート管理の活用により、データを更新してもアプリのビルドを更新せずに変更が反映されるため、柔軟性が高まります。

この構成により、データの管理の柔軟性メモリの効率的な使用、および更新の容易さを兼ね備えた、効果的なデータ管理が可能となります。

Addressableのリモートサーバー

Addressableのリモート設定では、リモートサーバーを使用してアセットバンドルをホストし、ゲームの更新なしでデータを配信することが可能です。主に以下のようなサーバーが対応しており、UnityのAddressablesに最適です。

1. Unityのクラウドサービス

  • Unity Cloud Content Delivery (CCD): Unityが提供するクラウドベースのホスティングサービスで、特にAddressableとの連携が簡単で、Unityエディターから直接ビルドとデプロイが可能です。バージョン管理や配信ターゲティングが可能で、ユーザーに異なるバージョンを配信することもできます。
  • メリット:
    • Unityエディターとの統合が簡単
    • バージョン管理とリリース管理が可能
    • 安全で信頼性の高いデリバリー

2. Amazon S3 (AWS)

  • Amazon Simple Storage Service (S3): AWSのオブジェクトストレージサービスで、アセットバンドルを保存し、HTTP/HTTPS経由でリモートアクセスできます。
  • メリット:
    • 大容量ストレージでコスト効率が高い
    • 安全性が高く、アクセス制御やセキュリティオプションが充実
    • グローバルなエッジロケーションでの配信が可能(CloudFrontを併用)

3. Google Cloud Storage (GCS)

  • Google Cloud Storage: Google Cloudのオブジェクトストレージで、Amazon S3と同様にHTTP/HTTPS経由でアクセスできます。Firebase Hostingと連携することで、簡単に配信も可能です。
  • メリット:
    • Google Cloudの他のサービス(Firebase等)との連携がしやすい
    • 高速なアクセスと低レイテンシの配信が可能
    • グローバルに展開されたCDN(Content Delivery Network)も利用可能

4. Microsoft Azure Blob Storage

  • Azure Blob Storage: Microsoft Azureが提供するオブジェクトストレージサービスで、AWS S3やGCSと同様、HTTP/HTTPS経由でAddressableアセットをホスティングできます。
  • メリット:
    • 高度なアクセス制御とデータの冗長性
    • Azure CDNを活用した高速なデータ配信
    • 他のAzureサービス(Azure FunctionsやApp Service等)との連携

5. 他のWebサーバー

  • カスタムWebサーバー: NginxやApacheなどのHTTPサーバーを利用して、自前のサーバーやクラウドサーバーでホスティングすることも可能です。
  • メリット:
    • 既存のWebインフラストラクチャを使用できる
    • 柔軟な構成で、コストを抑えやすい
    • SSL/TLSによるセキュアな配信が可能

6. Firebase Hosting

  • Firebase Hosting: Googleが提供するリアルタイムアプリケーション向けのホスティングサービス。特に小規模プロジェクトやFirebaseを使っているアプリに適しています。
  • メリット:
    • Firebase SDKを使用してクライアントと簡単に統合
    • 高速なCDNを通じて配信
    • 簡単にセットアップでき、無料プランも利用可能

セットアップ方法の概要

  1. Addressablesの設定で「Remote Load Path」を設定し、選択したサーバーのURLを指定します。
  • 例: https://example-bucket.s3.amazonaws.com/[BuildTarget]/[filename]
  1. Addressablesのプロファイル設定で、開発用・本番用の異なるリモートパスを設定して、環境ごとに異なるアセットバンドルを使用可能にします。
  2. アセットバンドルをビルドして、作成されたファイルを選択したリモートサーバーにアップロードします。
  3. アップロード後、Addressablesの設定に従って、リモートからの読み込みが可能になります。

まとめ

Unity Cloud Content Delivery (CCD)は、Addressablesに最適化されており、特にUnityプロジェクトとの統合がしやすく、推奨されます。S3やGCSなどの他のオブジェクトストレージサービスも、グローバル配信や信頼性の観点から優れた選択肢です。

テキストアドベンチャーでのケース

テキストアドベンチャーゲームで、AddressableとUnity Cloud Content Delivery(CCD)を利用してリモートからシナリオデータ(例えば、CSVやScriptableObjectのノードデータ)を配信・更新する流れについて、具体的な作業手順を解説します。

テキストアドベンチャーでのリモート配信の利点

  1. ストーリーやシナリオの追加:プレイヤーにゲームのアップデートを強制せず、新しいシナリオやエピソードを配信可能。
  2. 柔軟なエラー修正:ストーリーのミスや不具合を即座に修正し、リモート配信で反映できます。
  3. データの管理の簡略化:新しいエピソードや分岐の追加が簡単で、構成も柔軟になります。

導入手順

1. シナリオデータの準備

テキストアドベンチャーのシナリオデータとしては、以下の2つの方法を考えます。

  1. CSVファイル:テキストアドベンチャーの各シーン(ノード)や選択肢を一括で管理するためにCSVを使用。Excelなどで編集でき、追加・修正がしやすいです。
  2. ScriptableObject:各シーンをScriptableObjectで表現し、個々のシーンデータをCCD経由でリモート配信。CSVをインポートしてScriptableObjectを生成し、Addressableに登録してリモート配信することも可能です。

2. Addressableの設定

  1. Addressableパッケージのインストール: Unity Package Managerで「Addressables」をインストールします。
  2. CSVファイルやScriptableObjectのアセットをAddressableに設定:
  • CSVファイルやScriptableObjectをUnityのAssets/Resources/フォルダに保存し、インスペクタで「Addressable」にチェックを入れます。
  • 各データにユニークな名前を設定します(例: StoryDataCSV, Node_0, Node_1 など)。

3. Unity Cloud Content Delivery(CCD)の設定

  1. Unity Dashboardでプロジェクト作成:Unity Dashboardでテキストアドベンチャー用プロジェクトを作成し、「Cloud Content Delivery」を有効にします。
  2. 環境(Environment)設定:開発、ステージング、本番など、必要な環境を作成します。
  3. バケットの作成:シナリオデータを保存するバケットを作成し、各環境ごとに異なるデータを配信可能にします。

4. Addressableのプロファイル設定

  1. プロファイルの作成:「Profiles」ウィンドウでデフォルトのプロファイルを複製し、「CCD_Development」「CCD_Production」などのプロファイルを作成。
  2. リモートロードパスの設定:「RemoteLoadPath」を以下のように設定します。
  • https://[environment-id].cloud.unity3dusercontent.com/[bucket-id]/[filename]
  • 例: https://ccd.cloud.unity3dusercontent.com/production/[bucket-id]/[filename]
  • environment-idbucket-idはUnity DashboardのCCDセクションで確認できます。

5. シナリオデータのビルドとCCDへのアップロード

  1. アセットバンドルのビルド:「Addressables Groups」ウィンドウで「Build > New Build > Default Build Script」を選択し、アセットバンドルをビルドします。
  2. ビルドしたデータをCCDにアップロード
  • ビルド後に生成されるServerDataフォルダ内のファイルを、Unity DashboardのCCDバケットにアップロードします。
  • CCDで新しい「Release」を作成し、リモート配信の準備を完了します。

6. シナリオデータの非同期ロードとテキストアドベンチャーの進行

テキストアドベンチャーでシナリオを進めるとき、Addressable経由で非同期にノード(シーン)をロードします。

スクリプト:TextAdventureController.cs
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class TextAdventureController : MonoBehaviour
{
    public TextMeshProUGUI storyText;
    public Button choiceButton1;
    public Button choiceButton2;

    private StoryNode currentNode;

    void Start()
    {
        LoadStoryNode("Node_0"); // 開始ノードをリモートからロード
        choiceButton1.onClick.AddListener(() => OnChoiceMade(1));
        choiceButton2.onClick.AddListener(() => OnChoiceMade(2));
    }

    void LoadStoryNode(string nodeID)
    {
        Addressables.LoadAssetAsync<StoryNode>(nodeID).Completed += OnNodeLoaded;
    }

    void OnNodeLoaded(AsyncOperationHandle<StoryNode> handle)
    {
        if (handle.Status == AsyncOperationStatus.Succeeded)
        {
            currentNode = handle.Result;
            UpdateStory();
        }
        else
        {
            Debug.LogError("ノードの読み込みに失敗しました: " + handle.OperationException);
        }
    }

    void UpdateStory()
    {
        if (currentNode == null)
        {
            storyText.text = "冒険はここで終わりです。";
            choiceButton1.gameObject.SetActive(false);
            choiceButton2.gameObject.SetActive(false);
            return;
        }

        storyText.text = currentNode.storyText;
        choiceButton1.GetComponentInChildren<TextMeshProUGUI>().text = currentNode.choice1Text;
        choiceButton2.GetComponentInChildren<TextMeshProUGUI>().text = currentNode.choice2Text;
    }

    void OnChoiceMade(int choice)
    {
        string nextNodeID = (choice == 1) ? currentNode.choice1NextNode : currentNode.choice2NextNode;
        Addressables.Release(currentNode); // 使用後に解放
        LoadStoryNode(nextNodeID); // 次のノードをロード
    }
}

7. リモートでのシナリオ更新

新しいストーリーや分岐を追加した場合、再度ビルドしてCCDにアップロードすることで、ゲームのアップデートなしに新しいシナリオを配信できます。Addressableの管理で、常に最新のリリースを配信するように設定できます。

まとめ

このように、CCDを利用してテキストアドベンチャーのシナリオデータをAddressable経由で配信することで、アセットの柔軟な更新と効率的な管理が可能になります。特にシナリオデータの追加や修正が頻繁に行われる場合、CCDとAddressableを活用することで、ユーザー体験の向上が期待できます。

C#,Unity

Posted by hidepon