見出し画像

【Unity】xNode入門④ | 条件によって会話を変える方法|会話回数・好感度・時刻で分岐する

これまでの記事では、xNodeを使って会話ノードや選択肢ノードを作り、ノードの接続順に会話を進める仕組みを確認しました。

ノードをつなぐことで会話の流れを視覚的に管理できますが、通常の会話ノードだけでは、ゲームの状態が変わっても毎回同じ会話が表示されます。

実際のゲームでは、次のように条件によって会話を変えたい場面があります。

  • 初めて話したときだけ自己紹介する

  • 2回目以降は別のあいさつを表示する

  • 好感度が高いと特別な会話を発生させる

  • 朝・昼・夜であいさつを変える

  • イベントの進行状況によって話題を変える

そこで今回は、条件を判定する ConditionNode を作り、判定結果によって次に進む会話ノードを切り替えます。

今回扱う条件は、次の3つです。

  • 会話回数

  • 好感度

  • 時刻

条件分岐の基本が分かれば、所持アイテム、クエストの進行状況、プレイヤーのレベルなどにも応用できます。



1.今回のサンプルの仕組み

1-1.ConditionNode

これまでの会話グラフでは、基本的に1つの出力ポートから次のノードへ進んでいました。

今回は、条件判定用のノードに2つの出力ポートを用意します。

条件を確認
    ├ True  → 条件を満たした場合の会話
    └ False → 条件を満たさなかった場合の会話

たとえば、好感度が50以上かどうかを確認する場合は、次のように分岐します。

好感度が50以上?
    ├ True  → 特別な会話
    └ False → 通常の会話

条件を満たしている場合はTrue側、満たしていない場合はFalse側へ進みます。

今回作るConditionNodeは、会話文を表示するノードではなく、現在のゲーム状態を確認し、次に進むノードを決めるためのノードです。


1-2.ゲームの状態管理

条件分岐を行うには、現在の会話回数や好感度、時刻をどこかに保存しておく必要があります。
今回は、これらの値を GameStateManager でまとめて管理します。

役割を次のように分けます。

GameStateManager

  • 現在の会話回数を保存する

  • 現在の好感度を保存する

  • 現在時刻を保存する

ConditionNode

  • 何を判定するか設定する

  • どの値と比較するか設定する

  • TrueとFalseの接続先を持つ

DialogueController

  • ConditionNode の設定を読み取る

  • GameStateManager から現在値を取得する

  • 条件を判定する

  • 次に進むノードを決める

xNodeのグラフ内にゲーム全体の状態を直接保存するのではなく、GameStateManagerで管理している値をDialogueControllerが取得し、ConditionNodeの設定に従って次の会話を決定します。。


2.サンプルの作成

2-1.GameStateManagerを作る

Projectウィンドウでスクリプトファイル:GameStateManager.cs を作成し、次のように記述します。

public class GameStateManager : MonoBehaviour
{
    // Singletonパターンのインスタンスを保持するプロパティ
    public static GameStateManager Instance { get; private set; } 
    
    [Header("会話情報")]
    public int talkCount;   // 会話の回数を保持する変数

    [Header("キャラクター情報")]
    public int affection;   // キャラクターの好感度を保持する変数

    [Header("時間情報")]
    [Range(0, 23)]
    public int hour = 12;   // 時間を保持する変数(0〜23の範囲で設定可能)

    void Awake()
    {
        // すでにインスタンスが存在する場合は、重複を避けるために破棄
        if (Instance != null && Instance != this)   
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;    // Singletonインスタンスを設定
    }
}

コードの解説

今回は、次の3つの値を変数で管理します。

■ talkCount

キャラクターと会話した回数です。
初回と2回目以降で会話を変える場合に使用します。

■ affection

キャラクターの好感度です。
好感度が一定以上になったとき、特別な会話へ進ませることができます。

■ hour

ゲーム内の現在時刻です。

[Range(0, 23)]
public int hour = 12;

0時から23時までの整数で管理します。
Range属性を付けているため、Inspectorではスライダーを使って値を変更できます。
今回は条件分岐の動作を確認するため、Inspectorから時刻を変更します。


InstanceでGameStateManagerを参照する

次の部分では、ほかのスクリプトからGameStateManagerへアクセスできるようにしています。

public static GameStateManager Instance { get; private set; }

Awakeメソッドで、現在のGameStateManagerをInstanceへ登録します。

void Awake()
{
    if (Instance != null && Instance != this)
    {
        Destroy(gameObject);
        return;
    }

    Instance = this;
}

これにより、ほかのスクリプトから次のように現在の値を取得できます。

int talkCount = GameStateManager.Instance.talkCount;
int affection = GameStateManager.Instance.affection;
int hour = GameStateManager.Instance.hour;

今回は、GameStateManagerの値をInspectorから変更して他のスクリプトから参照できるように、GameStateManager本体をInstanceへ登録しています。
static変数だけで管理することもできますが、その場合はInspectorに値が表示す検証しにくいため、今回はInstance方式を使用します。


2-2.GameStateManagerをシーンに配置

Hierarchyウィンドウで右クリックし、空のGameObjectを作成し、名前をGameStateManager に指定します。
作成したGameObjectへ、GameStateManagerスクリプトをアタッチします。
※最新のUnityEditorでは、スクリプトファイル:GameStateManagerをそのままHierarchyにドラッグ&ドロップでも同じ結果になります。

Inspectorには、次の項目が表示されます。

最初は、次の値にしておきます。

Talk Count:0
Affection:0
Hour:12

これで、条件判定に使用する値を用意できました。


2-3.条件の種類を作る

次に、ConditionNode で何を判定するか選択できるようにします。

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

public enum ConditionType // 条件の種類を表す列挙型変数
{
    TalkCount,  // 会話回数を表す条件
    Affection,  // 好感度を表す条件
    Hour        // 時間を表す条件
}

enumを使うことで、ConditionNode の Inspector から条件の種類を選択できます。


2-4.比較方法を作る

会話回数や好感度を判定するときは、現在値と基準値を比較します。
たとえば、次のような条件です。

  • 会話回数が 1以上

  • 好感度が 50以上

  • 時刻が 17より小さい

比較方法を選択できるように、新しいC#スクリプトファイル:ComparisonType.cs を作成し、次のように記述します。

public enum ComparisonType  // 比較の種類を表す列挙型変数
{
    Equal,          // 等しいことを表す比較(==)
    NotEqual,       // 等しくないことを表す比較(!=)
    Greater,        // より大きいことを表す比較(>)
    GreaterOrEqual, // 以上を表す比較(>=)
    Less,           // より小さいことを表す比較(<)
    LessOrEqual     // 以下を表す比較(<=)
}

好感度が50以上という条件なら、Inspectorでは次の設定になります。

Condition Type:Affection
Comparison Type:Greater Or Equal
Target Value:50

2-5.ConditionNodeを作る

条件の種類と比較方法を用意できたので、条件判定用のノードを作ります。

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

using XNode;

[CreateNodeMenu("Dialogue/Condition")]  
public class ConditionNode : Node
{
    [Input] public DialogueNode input; // 入力ポート

    public ConditionType conditionType;     // 条件の種類を表す列挙型変数
    public ComparisonType comparisonType;   // 比較の種類を表す列挙型変数
    public int targetValue;

    [Output]  public DialogueNode trueOutput;   // 出力ポート(条件が真の場合)
    [Output]  public DialogueNode falseOutput;  // 出力ポート(条件が偽の場合)

}

※DialogueNodeの部分は、これまで作成した会話ノードのクラス名に合わせてください。


コードの解説

ConditionNodeには、1つの入力ポートと2つの出力ポートがあります。

[Input] public DialogueNode input;

inputは、前のノードからConditionNodeへ入るためのポートです。

出力ポートは、次の2つです。

[Output] public DialogueNode trueOutput;
[Output] public DialogueNode falseOutput;
  • trueOutput:条件を満たしたときに進む接続先です。

  • falseOutput:条件を満たさなかったときに進む接続先です。

グラフ上では、次のような構成になります。

ConditionNodeには、条件を設定するための3つの項目もあります。

public ConditionType conditionType;
public ComparisonType comparisonType;
public int targetValue;

■ conditionType

何を判定するか選択します。

■ comparisonType

現在値と基準値をどのように比較するか選択します。

■ targetValue

比較する基準値を設定します。
たとえば、好感度が50以上かどうかを判定する場合は、以下のように設定します。

  • Condition Type を Affection

  • Comparison Type を Greater Or Equal

  • Target Value を 50 


2-6.DialogueGraphを作る

条件分岐の動作を分かりやすく確認するため、新しいDialogueGraphを作成します。
Projectウィンドウで右クリックし、次の順番で作成します。

Create → Dialogue Graph

作成したグラフの名前を、ConditionDialogueGraph にします。

このグラフに、今回作成するConditionNodeや会話ノードを配置していきます。

2-7.数値を比較する処理を作る

ここからは、前の記事で作成したDialogueControllerに条件判定の処理を追加します。

※作成していない場合はこちらをご参照ください

DialogueController.csを開き、現在値と基準値を比較する CompareValuesメソッドを追加します。

// 値を比較するメソッド
bool CompareValues(int currentValue,int targetValue, ComparisonType comparisonType) 
{
    switch (comparisonType) // comparisonTypeに応じて値を比較
    {
        case ComparisonType.Equal:      // 等しい場合
            return currentValue == targetValue; 

        case ComparisonType.NotEqual:   // 等しくない場合
            return currentValue != targetValue;

        case ComparisonType.Greater:    // 大きい場合
            return currentValue > targetValue;

        case ComparisonType.GreaterOrEqual: // 以上の場合
            return currentValue >= targetValue;

        case ComparisonType.Less:       // 小さい場合
            return currentValue < targetValue;

        case ComparisonType.LessOrEqual:    // 以下の場合
            return currentValue <= targetValue;

        default:    // それ以外の場合
            return false;
    }
}

コードの解説

このメソッドでは、次の3つの値を受け取ります。

■ currentValue

GameStateManager に保存されている現在値です。
たとえば、現在の好感度が70なら、currentValueは70になります。

■ targetValue

ConditionNodeに設定した基準値です。
好感度が50以上かどうかを確認する場合は、50になります。

■ comparisonType

どのように比較するかを表します。
たとえば、GreaterOrEqualが選択されている場合は、次の比較を行います。

currentValue >= targetValue

現在の好感度が70、基準値が50なら、実際の比較は次のようになります。

70 >= 50

条件を満たしているため、true を返します。
一方、現在の好感度が30なら、次の比較になります。

30 >= 50

この場合は条件を満たしていないため、false を返します。


2-8.ConditionNodeの条件を判定する

次に、ConditionNodeで選択された条件に応じて、GameStateManagerから現在値を取得します。

DialogueControllerクラスの中へ、次の CheckConditionメソッドを追加します。

// ConditionNodeの条件をチェックするメソッド
bool CheckCondition(ConditionNode node) 
{
    GameStateManager gameState = GameStateManager.Instance;

    if (gameState == null)  return false;

    switch (node.conditionType) // conditionTypeに応じて条件をチェック
    {
        case ConditionType.TalkCount:   // 会話回数の場合
            return CompareValues(
                gameState.talkCount,    // 現在の会話回数
                node.targetValue,       
                node.comparisonType
            );

        case ConditionType.Affection:   // 好感度の場合
            return CompareValues(
                gameState.affection,    // 現在の好感度
                node.targetValue,
                node.comparisonType
            );

        case ConditionType.Hour:        // 時間の場合
            return CompareValues(
                gameState.hour,         // 現在の時間
                node.targetValue,
                node.comparisonType
            );

        default:
            return false;
    }
}

コードの解説

GameStateManager gameState = GameStateManager.Instance;

最初に、GameStateManagerのインスタンスを取得します。
これにより、次の値を参照できるようになります。

  • gameState.talkCount:会話回数

  • gameState.affection:好感度

  • gameState.hour:時間


if (gameState == null)   return false;

GameStateManagerがシーンに配置されていない場合は、現在値を取得できないので、false を返します。


switch (node.conditionType)

switch文では、ConditionNode の conditionType を確認します。

case ConditionType.TalkCount:
    return CompareValues(
        gameState.talkCount,
        node.targetValue,
        node.comparisonType
    );

TalkCountが選択されている場合は、現在の会話回数を比較します。
同様にAffectionなら好感度、Hourなら現在時刻を比較します。

比較方法は、先ほど作成したCompareValuesメソッドへ任せています。


2-9.TrueとFalseの接続先を取得する

条件を判定したら、その結果に応じて次に進むノードを取得します。

DialogueControllerクラスの中へ、次のメソッドを追加します。

// ConditionNodeの次のノードを取得するメソッド
Node GetConditionNextNode(ConditionNode node)   
{
    bool result = CheckCondition(node); // 条件をチェックして結果を取得
    // 条件がtrueの場合はtrueOutput、falseの場合はfalseOutputのポート名を取得
    string portName = result            
        ? nameof(node.trueOutput)
        : nameof(node.falseOutput);

    // 指定されたポート名の出力ポートを取得
    NodePort outputPort = node.GetOutputPort(portName); 

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

    return outputPort.Connection.node;
}

コードの解説

bool result = CheckCondition(node);

上で作ったCheckConditionを使って条件を判定、trueまたはfalseを返します。


string portName = result
    ? nameof(node.trueOutput)
    : nameof(node.falseOutput);

三項演算子を使って、使用する出力ポートを選びます。
条件を満たした場合は、trueOutput を選択します。
条件を満たさなかった場合は、falseOutput を選択します。

三項演算子を使わずに書くと、次の処理と同じです。

string portName;

if (result)
{
    portName = nameof(node.trueOutput);
}
else
{
    portName = nameof(node.falseOutput);
}

NodePort outputPort = node.GetOutputPort(portName);

GetOutputPortを使い、選択した出力ポートを取得します。


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

ポートが接続されていない場合は、次のノードが存在しないため、nullを返します。


return outputPort.Connection.node;

接続されている場合は、接続先のノードを返します。


2-10.ConditionNodeを会話進行に追加する

最後に、会話の進行処理で ConditionNode を認識できるようにします。

これまで作成したDialogueControllerでは、ShowCurrentNodeメソッドの中で、現在のノードがDialogueNode、ChoiceNode、EndNodeのどれであるかを確認していました。
今回は、この処理にConditionNodeの判定を追加します。

ShowCurrentNodeメソッドを、次のように修正します。

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

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

    /*ここまで記述済み*/     

    // 現在のノードがConditionNodeの場合
    if (currentNode is ConditionNode conditionNode)
    {
        // 条件判定の結果に応じて、次のノードを取得
        currentNode = GetConditionNextNode(conditionNode);

        // 取得した次のノードを続けて処理
        ShowCurrentNode();
        return;
    }

    /*以下記述済み*/

    if (currentNode is DialogueNode dialogueNode)
    {
        messageText.text = dialogueNode.message;
        nextButton.gameObject.SetActive(true);
    }
    else if (currentNode is ChoiceNode choiceNode)
    {
        messageText.text = choiceNode.question;

        yesButton.gameObject.SetActive(true);
        noButton.gameObject.SetActive(true);
    }
    else if (currentNode is EndNode)
    {
        EndDialogue();
    }
}

今回追加したのは、次の部分です。

if (currentNode is ConditionNode conditionNode)
{
    currentNode = GetConditionNextNode(conditionNode);
    ShowCurrentNode();
    return;
}

現在のノードが ConditionNode だった場合、[2-8]で作った GetConditionNextNodeメソッドを使って、条件に合った接続先を取得します。

currentNode = GetConditionNextNode(conditionNode);

条件を満たしていれば trueOutput側、満たしていなければ falseOutput側 のノードが返されます。

ShowCurrentNode();

取得したノードを currentNode へ設定したあと、ShowCurrentNode をもう一度呼び出します。

ConditionNode自身は会話文や選択肢を表示するノードではありません。
そのため、ConditionNodeに到達した時点ではボタン入力を待たず、条件を判定して次のノードへ進みます。

これで、DialogueControllerがConditionNodeを認識し、条件に応じて会話の進行方向を切り替えられるようになりました。


3.会話回数によって内容を変える

最初に、会話回数による分岐を作ります。
今回は、初めて話しかけた場合と、2回目以降で会話を変えます。

3-1.会話グラフを作る

会話内容は次のようにします。

初回の会話

「こんにちは。初めて見る顔ですね。」

2回目以降の会話

「また来てくれたんですね。」

会話グラフを次のように接続します。

ConditionNode の Inspectorを、次のように設定します。

Condition Type:Talk Count
Comparison Type:Greater Or Equal
Target Value:1

これは、現在の会話回数が1以上かどうかを確認する設定です。

Talk Countが1以上?
    ├ True  → 2回目以降の会話
    └ False → 初回の会話

最初はTalk Countが0なので、False側へ進みます。


3-2.オブジェクトの配置

ではこの動きをプレイをして確認してみます。

前回の記事同様、次のようにUIオブジェクトを配置します。

  • Canvas

    • Panel

      • MesseageText

      • NextButton

      • YesButton

      • NoButton

さらにHierarchyに 以下の3つのスクリプトファイルを持つオブジェクトが配置済みであることを確認します。

  • GameStateManagerを持つオブジェクト

  • DialogueControllerを持つオブジェクト

DialogueController のInspectorから各オブジェクトをセットします。

この状態でプレイしてみます。
TalkCount が 0 の状態では、最初のメッセージが表示されます。

Inspectorで TalkCount を1 以上にすると、2回目以降に設定したメッセージが表示されます。


3-3.会話回数を増やす

会話回数を動的に更新するには、会話が終了したときにtalkCountを増やす必要があります。

DialogueController の ShowCurrentNode()メソッド で EndNode を処理する部分に、次のコードを追加します。

 else if (currentNode is EndNode)    // 現在のノードがEndNodeの場合
 {
     /*この条件文を追加*/
     if (GameStateManager.Instance != null)// GameStateManagerのインスタンスが存在する場合
     {
         GameStateManager.Instance.talkCount++; // 会話回数をインクリメント
     }
     EndDialogue();  // 会話を終了する(記述済み)
 }

これにより、EndNodeへ到達すると会話回数が1増えます。

初回の会話終了
Talk Count:0 → 1

2回目の会話終了
Talk Count:1 → 2

会話を開始した時点ではなく、最後まで終了した時点で回数を増やす形です。


4.好感度によって内容を変える

次は、好感度が50以上かどうかで会話を変えます。

4-1.会話グラフを作る

会話内容は次のようにします。

好感度50未満

「何か用ですか?」

好感度50以上

「 来てくれたんですね。 待っていました。」

グラフを次のように接続します。

ConditionNodeのInspectorを、次のように設定します。

Condition Type:Affection
Comparison Type:Greater Or Equal
Target Value:50

4-2.Inspectorから好感度を変更する

今回は条件分岐の確認が目的なので、好感度はGameStateManagerのInspectorから変更します。

最初に、次の値を設定します。

Affection:30

この場合は条件を満たしていないため、False側の会話が表示されます。

次に、Affectionを70へ変更します。

Affection:70

今度は条件を満たしているため、True側の会話が表示されます。

実際のゲームでは、選択肢やイベントの結果によって好感度を増減させます。


5.時刻によって内容を変える

次は、ゲーム内時刻によってあいさつを変更します。

今回は、朝・昼・夜の3種類に分けます。

  • 0時から10時:朝

  • 11時から16時:昼

  • 17時から23時:夜

5-1.朝かどうかを判定する

最初のConditionNodeでは、現在時刻が11時より前かを確認します。

Condition Type:Hour
Comparison Type:Less
Target Value:11

条件を満たした場合は、朝の会話へ進みます。

「おはようございます。 今日は早いですね。」

条件を満たさなかった場合は、昼または夜なので、2つ目のConditionNodeへ進みます。


5-2.昼か夜かを判定する

2つ目のConditionNodeは、次のように設定します。

Condition Type:Hour
Comparison Type:Less
Target Value:17

17時より前なら昼、17時以降なら夜です。

会話グラフ全体は、次のようになります。

StartNode
    ↓
Hour < 11
    ├ True  → 朝の会話
    │
    └ False
         ↓
       Hour < 17
         ├ True  → 昼の会話
         └ False → 夜の会話

昼の会話

「こんにちは。 いい天気ですね。」

夜の会話

「こんばんは。 もう遅い時間ですよ。」

GameStateManager の Inspector から Hourを変更して、表示される会話を確認します。

このように、TrueとFalseの2つの出力しか持たないConditionNodeでも、複数つなげることで3種類以上に分岐できます。


6.今回のまとめ

今回は、xNodeでゲームの状態を確認し、条件によって会話内容を変える方法を確認しました。

ポイントは次のとおりです。

今回扱った条件は、会話回数、好感度、時刻の3種類です。

ConditionTypeへ項目を追加し、CheckConditionに判定処理を追加すれば、次のような条件にも応用できます。

  • アイテムを持っているか

  • クエストを達成しているか

  • プレイヤーのレベルが一定以上か

  • 特定のイベントを見たか

条件分岐を加えることで、同じキャラクターに話しかけた場合でも、ゲームの進行状況に応じて異なる会話を表示できるようになります。


次回予告

次回は、xNodeから会話画面の演出を変更する方法を確認します。

会話の進行に合わせて、次の要素を切り替えます。

  • キャラクターの立ち絵

  • 表情

  • 背景画像

  • BGM

今回作成した条件分岐と組み合わせることで、時刻によって背景やBGMを変えたり、好感度が高い場合だけ特別な表情を表示したりできるようになります。


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


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

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


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


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

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