見出し画像

【Unity】xNode入門③|Connectionをたどって会話を進める|選択肢で分岐する仕組みを作る

前回は、xNode の Node同士で値を受け渡す方法を確認しました。

今回は、Connectionの使い方を一歩進めて「接続されている次のNodeへ進む」という仕組みを作ります。

最終的には、会話の内容や選択肢、分岐先をC#スクリプトへ直接書かず、xNodeのGraph上でScenarioを組み立てられるようにすることが目標です。

今回はその途中段階として、

  • StartNodeから会話を開始する

  • DialogueNodeの文章をUIへ表示する

  • Buttonを押して次のNodeへ進む

  • ChoiceNodeで「はい」「いいえ」に分岐する

  • EndNodeで会話を終了する

ところまで確認します。



1.xNodeとUnityの役割

今回のSampleでは、xNode側とUnity側で役割を分けます。

xNode側

xNodeのGraphには、次の情報を保存します。

  • 会話の開始位置

  • 表示する文章

  • 選択肢

  • 次に進むNode

  • 「はい」を選んだときの進行先

  • 「いいえ」を選んだときの進行先

  • 会話の終了位置

Unity側

Unity側のC#スクリプトは、次の処理を担当します。

  • 現在のNodeを確認する

  • Nodeの内容をUIへ表示する

  • Buttonが押されたらConnectionを調べる

  • Connectionの接続先を次のNodeとして設定する

  • Nodeの種類に応じてUIを切り替える


2.値のConnectionと進行用のConnection

前回は、次のようなGraphを作りました。

この Connection は、AddNode や SubtractNode が NumberNode から値を取得するために使われています。
今回作るConnectionは、少し役割が違います。

DialogueNode
     │
     ▼
ChoiceNode
     │
     ▼
DialogueNode

今回は Connection を、次に進むNodeを示す線 として利用します。
整理すると、次のようになります。

  • 第2回 Connectionをたどる → 接続先から値を取得する

  • 第3回 Connectionをたどる → 接続先のNodeへ進む

ただし、線をつないだだけで会話が自動的に進むわけではありません。
C#側の Controller が Connection を確認し、接続先のNodeを取得して、現在のNodeを切り替えます。


3.サンプルの作成

3-1 会話用のGraphを作る

今回は会話専用のGraphとして「DialogueGraph」を新しく作ります。
スクリプトファイル:DialogueGraph.cs を作成し、次のように記述します。

using UnityEngine;
using XNode;

[CreateAssetMenu]
public class DialogueGraph : NodeGraph
{
}

NodeGraph を継承することで、xNodeのGraphとして使用できます。
スクリプトのコンパイルが完了したら、Projectウィンドウで右クリックします。

Create → Dialogue Graph

から、新しいGraphアセットを作成します。

名前は、Scenario Dialogue Graph など任意の名前で構いません。
このGraphに、会話の流れを作っていきます。


3-2.使用するNodeの確認

今回は、次の4種類のNodeを作ります。

  • StartNode

  • DialogueNode

  • ChoiceNode

  • EndNode

それぞれの役割を確認しておきましょう。

■ StartNode

会話の開始位置を表します。
StartNode から最初の DialogueNode に進みます

■ DialogueNode

画面へ表示する文章を持つNodeです。

■ ChoiceNode

質問文と、複数の分岐先を持つNodeです。

■ EndNode

会話の終了位置を表します。
ControllerがEndNodeへ到達したら、会話を終了します。


3-3.StartNodeを作る

最初に、会話の開始位置となるStartNodeを作ります。

スクリプトファイル:StartNode.cs を作成し、次のように記述します。

using XNode;

public class StartNode : Node
{
    [Output] public Node next;  // 次のノードへの接続を表す出力ポート

    public Node GetNextNode()   // 次のノードを取得するメソッド
    {
        NodePort port = GetOutputPort("next");  // "next"という名前の出力ポートを取得
 
        // ポートが存在しないか、接続されていない場合は null を返す
        if (port == null || !port.IsConnected) { return null; }

        return port.Connection.node;    // 接続されているノードを返す
    }
}

コードの解説

[Output] public Node next;

この部分で、次に進むNodeを接続するOutput Portを作っています。
Graph Editor上では、NextというOutput Portが表示されます。

このPortに最初のDialogueNodeを接続します。


今回の中心になるのが、次の処理です。

public Node GetNextNode()   // 次のノードを取得するメソッド
{
    NodePort port = GetOutputPort("next"); 
    if (port == null || !port.IsConnected) { return null; }
    return port.Connection.node;  
}

順番に確認してみます。

NodePort port = GetOutputPort("next");

GetOutputPort() を使うと、指定した名前のOutput Portを取得できます。
今回取得しているのは、

[Output] public Node next;

で宣言した next Portです。


!port.IsConnected

そのPortにConnectionが設定されているか確認しています。
接続されていない状態で接続先を取得しようとすると、エラーの原因になります。そこで、

if (port == null || !port.IsConnected){ return null; }

として、Portが見つからない場合や接続されていない場合は null を返しています。


return port.Connection.node;

port.Connection で接続先のPortを取得できます。
さらに、そのPortの、

.node

を調べることで、接続先のNodeを取得できます。


3-4.DialogueNodeを作る

続いて、会話文を保存するDialogueNodeを作ります。

スクリプトファイル:DialogueNode.cs を作成します。

using UnityEngine;
using XNode;

[CreateNodeMenu("Dialogue/Message")]
public class DialogueNode : Node
{
    [Input] public Node input;  // 前のノードからの接続を表す入力ポート

    [TextArea] public string message;   // メッセージを格納するテキストエリア

    [Output] public Node next;  // 次のノードへの接続を表す出力ポート

    public Node GetNextNode()   // 次のノードを取得するメソッド
    {
        NodePort port = GetOutputPort("next");  // "next"という名前の出力ポートを取得

        // ポートが存在しないか、接続されていない場合はnullを返す
        if (port == null || !port.IsConnected){ return null; }  

        return port.Connection.node;    // 接続されているノードを返す
    }
}

コードの解説

[Input] public Node input; 

前のノードからの接続ポートです。


[TextArea] public string message;

message には、画面へ表示する文章を設定します。
[TextArea] を付けることで、Graph Editor上で文章を複数行入力しやすくなります。
今回の例では、「目の前に宝箱があります。カギはかかっていないようだ」とします。


[Output] public Node next;

next は、次に進むNodeを接続するOutput Portです。

DialogueNodeでは、ユーザーが「次へ」Buttonを押したときに、GetNextNode() を使って接続先のNodeを取得します。


3-5.ChoiceNodeを作る

次に、選択肢を表示するChoiceNodeを作ります。今回は、

  • はい

  • いいえ

の2つに分岐します。

スクリプトファイル:ChoiceNode.cs を作成し、次のように記述します。

using UnityEngine;
using XNode;

public class ChoiceNode : Node
{
    [Input] public Node input;  // 前のノードからの接続を表す入力ポート
    [TextArea] public string question;  // 質問を格納するテキストエリア
    [Output] public Node yes;   // "yes"の選択肢への接続を表す出力ポート
    [Output] public Node no;    // "no"の選択肢への接続を表す出力ポート

    public Node GetYesNode()    // "yes"の選択肢を取得するメソッド
    {
        return GetConnectedNode("yes"); // "yes"という名前の出力ポートを取得
    }

    public Node GetNoNode()     // "no"の選択肢を取得するメソッド
    {
        return GetConnectedNode("no");  // "no"という名前の出力ポートを取得
    }

    // 指定されたポート名に接続されているノードを取得するメソッド
    Node GetConnectedNode(string portName)  
    {
        NodePort port = GetOutputPort(portName);  // 指定された名前の出力ポートを取得

        // ポートが存在しないか、接続されていない場合はnullを返す
        if (port == null || !port.IsConnected) { return null; }

        return port.Connection.node;    // 接続されているノードを返す
    }
}

コードの解説

[Input] public Node input; 

前のノードからの接続ポートです。


[TextArea] public string question;

ここには、画面へ表示する質問文を設定します。
今回の例では「宝箱を開けますか?」とします。


[Output] public Node yes;

[Output] public Node no;

これによって、2つのOutput Portが作成されます。

Yes Portには「はい」を選んだ場合の進行先を接続します。
No Portには「いいえ」を選んだ場合の進行先を接続します。


ChoiceNodeには、次の2つのメソッドがあります。

public Node GetYesNode()
{
    return GetConnectedNode("yes");
}
public Node GetNoNode()
{
    return GetConnectedNode("no");
}

「はい」が選択された場合は、yes Portの接続先を取得します。
「いいえ」が選択された場合は、no Portの接続先を取得します。


実際に接続先を取得しているのは、次の共通メソッドです。

private Node GetConnectedNode(string portName)
{
    NodePort port = GetOutputPort(portName);
    if (port == null || !port.IsConnected) { return null; }
    return port.Connection.node;
}

引数の portName に、yes または、no が渡されます。
これによって、Buttonによって取得する接続先を切り替えられます。

  • 「はい」を押す → yes Portを確認  → yes Portの接続先へ進む

  • 「いいえ」を押す → no Portを確認 → no Portの接続先へ進む


3-6.EndNodeを作る

最後に、会話の終了地点となるEndNodeを作ります。
スクリプトファイル:EndNode.cs を作成します。

using XNode;

public class EndNode : Node
{
    [Input] public Node input;  // 前のノードからの接続を表す入力ポート
}

EndNodeは会話の終了を示すだけなので、Input の Port だけを持たせています。

Controller側で、

currentNode is EndNode

となった場合に、Buttonを非表示にして会話を終了します。


3-7.DialogueGraphを組み立てる

ここまでで、4種類のNodeを作成しました。
作成した Scenario Dialogue Graph を開きます。

Graph Editor内で右クリックし、次のNodeを配置します。

  • StartNode × 1

  • DialogueNode × 3

  • ChoiceNode × 1

  • EndNode × 2

それぞれの文章を設定します。

DialogueNode 1

目の前に宝箱があります。

ChoiceNode

宝箱を開けますか?

DialogueNode 2

剣を手に入れました。

DialogueNode 3

宝箱を開けずに先へ進みました。

Nodeを次のように接続します。

これで、会話の流れと分岐先をGraph上に作成できました。


3-8.会話用のUIを作る

続いて会話内容を表示するUIを作ります。

Hierarchyで、Canvas を作成し、Panel を配置、その配下に Text と 3つの Button を配置します。
今回はサンプルなので、それぞれ Legacy のもので結構です。
それぞれのオブジェクトの名前を下の図のように指定しておきます。

■ MessageText

会話文や質問文を表示します。
名前は、MessageText としておきます。

■ NextButton

通常の DialogueNode から次の Node へ進むための Button です。
Buttonの文字は「次へ」とします。

■ YesButton

ChoiceNodeで「はい」を選択するButtonです。

■ NoButton

ChoiceNodeで「いいえ」を選択するButtonです。

通常のDialogueNodeを表示しているときは、NextButtonだけを表示します。

ChoiceNodeを表示しているときは、YesButtonとNoButtonを表示します。


3-9.DialogueControllerを作る

xNodeのGraphを読み取り、UIへ会話を表示するControllerを作ります。
スクリプトファイル:DialogueController.cs を作成し、次のように記述します。

using UnityEngine;
using UnityEngine.UI;
using XNode;

public class DialogueController : MonoBehaviour
{
    public DialogueGraph graph; // DialogueGraphの参照

    public Text messageText;    // 会話メッセージを表示するText

    public Button nextButton;   // 次の会話に進むボタン
    public Button yesButton;    // 選択肢「はい」のボタン
    public Button noButton;     // 選択肢「いいえ」のボタン

    Node currentNode;   // 現在のノードを保持する変数

    void Start()
    {
        StartDialogue();
    }

    public void StartDialogue()
    {
        // DialogueGraphが設定されていない場合は処理を中断
        if (graph == null) return; 

        StartNode startNode = null; // StartNodeを探す

        foreach (Node node in graph.nodes)  // DialogueGraph内のすべてのノードをチェック
        {
            if (node is StartNode foundStartNode)   // StartNodeが見つかった場合
            {
                startNode = foundStartNode; // StartNodeを保持
                break;
            }
        }

        if (startNode == null)  // StartNodeが見つからなかった場合
        {
            Debug.LogWarning("StartNodeが見つかりません。"); // 警告をログに出力
            return; // 処理を中断
        }

        // StartNodeの次のノードをcurrentNodeに設定
        currentNode = startNode.GetNextNode();  

        ShowCurrentNode();  // UIを更新して現在のノードを表示
    }

    void ShowCurrentNode()  // UIを現在のノードに応じて更新するメソッド
    {
        HideAllButtons();

        if (currentNode == null)
        {
            Debug.LogWarning("次のNodeが接続されていません。");
            EndDialogue();  // 会話を終了する
            return;         // 処理を中断
        }

        if (currentNode is DialogueNode dialogueNode)   // 現在のノードがDialogueNodeの場合
        {
            messageText.text = dialogueNode.message;    // メッセージを表示
            nextButton.gameObject.SetActive(true);      // 次の会話に進むボタンを表示
        }
        else if (currentNode is ChoiceNode choiceNode)  // 現在のノードがChoiceNodeの場合
        {
            messageText.text = choiceNode.question;     // 質問を表示

            yesButton.gameObject.SetActive(true);   // 選択肢「はい」のボタンを表示
            noButton.gameObject.SetActive(true);    // 選択肢「いいえ」のボタンを表示
        }
        else if (currentNode is EndNode)            // 現在のノードがEndNodeの場合
        {
            EndDialogue();  // 会話を終了する
        }
    }

    public void Next()  // Nextボタンが押されたときの処理
    {
        // 現在のノードがDialogueNodeでない場合は処理を中断
        if (currentNode is not DialogueNode dialogueNode) return;
   
        currentNode = dialogueNode.GetNextNode();   // 次のノードをcurrentNodeに設定

        ShowCurrentNode();  // UIを更新して現在のノードを表示
    }

    public void SelectYes() // 選択肢「はい」を選んだ場合の処理
    {
        // 現在のノードがChoiceNodeでない場合は処理を中断
        if (currentNode is not ChoiceNode choiceNode) return;

        currentNode = choiceNode.GetYesNode();  // 対応するノードをcurrentNodeに設定

        ShowCurrentNode();  // UIを更新して現在のノードを表示
    }

    public void SelectNo()
    {
        // 現在のノードがChoiceNodeでない場合は処理を中断
        if (currentNode is not ChoiceNode choiceNode) return;

        currentNode = choiceNode.GetNoNode();  // 対応するノードをcurrentNodeに設定

        ShowCurrentNode();  // UIを更新して現在のノードを表示
    }

    void HideAllButtons()   // すべてのボタンを非表示にするメソッド
    {
        nextButton.gameObject.SetActive(false);
        yesButton.gameObject.SetActive(false);
        noButton.gameObject.SetActive(false);
    }

    void EndDialogue()  // 会話を終了するメソッド
    {
        HideAllButtons();   // すべてのボタンを非表示にする

        messageText.text = "会話を終了しました。";
    }
}

少し長いコードなので、順番に確認していきます。


コードの解説

Node currentNode;

この変数には、現在表示しているNodeを保存します。
会話が進むたびに、この currentNode を接続先のNodeへ変更します。

Scenario の進行状況は、currentNode がどの Node を参照しているかによって決まります。


StartDialogue()内での処理

StartNode startNode = null;

foreach (Node node in graph.nodes)
{
    if (node is StartNode foundStartNode)
    {
        startNode = foundStartNode;
        break;
    }
}

会話を開始するときは、Graph内から StartNode を探します。
Graph に含まれる Node を1つずつ確認し、StartNode が見つかったら保存します。

if (startNode == null)
{
    Debug.LogWarning("StartNodeが見つかりません。");
    return;
}

StartNodeが見つからない場合は、警告を表示します。

currentNode = startNode.GetNextNode();

StartNodeが見つかった場合は、その接続先を最初のNodeとして設定します。そのあと、

ShowCurrentNode();

を呼び出して、最初の会話を表示します。


ShowCurrentNode()での処理

ShowCurrentNode() では、現在の Node がどの種類なのかを確認しています。

if (currentNode is DialogueNode dialogueNode)

現在の Node が DialogueNodeの場合は、会話文を表示します。

messageText.text = dialogueNode.message;
nextButton.gameObject.SetActive(true);

MessageText に文章を設定し、NextButton を表示します。

ChoiceNodeの場合は、質問文を表示します。

else if (currentNode is ChoiceNode choiceNode)
{
    messageText.text = choiceNode.question;

    yesButton.gameObject.SetActive(true);
    noButton.gameObject.SetActive(true);
}

この場合は NextButton を表示せず、YesButton と NoButton を表示します。

EndNodeの場合は、会話を終了します。

else if (currentNode is EndNode)
{
    EndDialogue();
}

このように、Nodeの種類によって表示内容やButtonを切り替えています。


Next() 内での処理

DialogueNodeを表示しているときにNextButtonを押すと、Next() が呼ばれます。

public void Next()
{
    if (currentNode is not DialogueNode dialogueNode)  return;
    currentNode = dialogueNode.GetNextNode();
    ShowCurrentNode();
}

現在のNodeがDialogueNodeであることを確認したあと、

dialogueNode.GetNextNode()

で、Next Portの接続先を取得します。
取得したNodeを、currentNode へ代入し、次のNodeを表示します。

処理の流れは次のようになります。

  1. NextButtonを押す

  2. 現在のDialogueNodeを確認

  3. next Portを取得

  4. Connectionの接続先を取得

  5. currentNodeを変更

  6. 次のNodeを表示


SelectYes() と SelectNo()

YesButtonを押すと、SelectYes() が呼ばれます。

public void SelectYes()
{
    if (currentNode is not ChoiceNode choiceNode) return;
    currentNode = choiceNode.GetYesNode();
    ShowCurrentNode();
}

現在の Node が ChoiceNode であることを確認し、

choiceNode.GetYesNode()

で、Yes Port の接続先を取得します。今回の Graph では、

Yes →「剣を手に入れました」

と接続しているため、YesButton を押すと「剣を手に入れました」と表示されます。


一方、NoButtonを押すと SelectNo() が呼ばれます。

public void SelectNo()
{
    if (currentNode is not ChoiceNode choiceNode) return;
    currentNode = choiceNode.GetNoNode();
    ShowCurrentNode();
}

今度は、

choiceNode.GetNoNode()

によって、No Port の接続先を取得します。今回のGraphでは、

No →「宝箱を開けずに先へ進みました」

と接続しています。
C#側には、

messageText.text = "宝箱を開けずに先へ進みました";

とは書かれていません。
どの文章を表示するかは、Graph 上の Connection によって決まっています。


3-10.ButtonのOn Clickに登録する

作成した DialogueController.cs を、空のGameObjectへアタッチします。
GameObject の名前は、DialogueController などにしておきます。

Inspectorで、次の項目を設定します。

  • Graph:作成した ScenarioDialogueGraph

  • Message Text:MessageText

  • Next Button:NextButton

  • Yes Button:YesButton

  • No Button:NoButton

次に、各Buttonの On Click() を設定します。

NextButton

DialogueController → Next()

YesButton

DialogueController → SelectYes()

NoButton

DialogueController → SelectNo()

これで、Button操作とGraph上のConnectionが連携します。


4.実行してみる

UnityをPlayします。

最初に、StartNodeの接続先となっているDialogueNodeが表示されます。

「次へ」を押すと、ChoiceNodeへ進みます。

「はい」を押した場合は、

と表示されます。

「いいえ」を押した場合は、

と表示されます。

これで、Graph上に作った会話の流れを、UnityのUIから実行できました。


接続されていない場合

NodeのPortを接続し忘れる可能性もあります。
今回の各Nodeでは、接続先がない場合に null を返すようにしています。

if (port == null || !port.IsConnected) return null;

Controller側でも、

if (currentNode == null)
{
    Debug.LogWarning("次のNodeが接続されていません。");
    EndDialogue();
    return;
}

と確認しています。
そのため、接続されていないPortへ進もうとした場合は、Consoleに警告を表示して会話を終了します。


5.今回のまとめ

今回は、xNodeのConnectionを使って、次のNodeへ進む仕組みを作りました。

今回のポイントは次の通りです。


次回は今回の仕組みをさらに発展させて、

  • 話者名

  • 会話本文

  • 背景画像

  • 複数の選択肢

  • 選択肢ごとの接続先

  • 回数、時間軸などの条件による発言内容

などをNodeで管理します。
最終的には、C#へScenario固有の内容を書かず、Nodeを配置してConnectionをつなぐことで、Scenarioを組み立てられる仕組み を作っていきます。


▼ Unity初心者向け記事まとめはこちら


こちらのマガジン「ミニゲーム作成講座 全8回セット」でもシーンの切り替えなどを活用して、初心者でも知識ゼロから作れるミニゲーム作成講座を紹介しています。

さらにステップアップの「スマホで遊ぶジャンプゲーム 8回セット」のマガジンもぜひご活用ください。


ファイブボックスでは、スクラッチやUnityの個別指導のオンラインレッスンを行っています。
ご興味のある方は当サイト、オンラインレッスンから、無料体験授業へお問い合わせ下さい。

いいなと思ったら応援しよう!

ファイブボックス よろしければ応援お願いします! いただいたチップはクリエイターとしての活動費に使わせていただきます!