Fungusを使ったゲーム制作におけるテスト作成と自動テスト環境の作成及び構築【Unity,Fungus】

はじめに

2021年5月から複数人で3D オープンワールドゲーム( DreamIsland ) [PVは以下動画]を制作しています。 ゲーム内では、ゲームでよくなる NPCとの会話やセーブ・ロード機能、メニュなどいろいろな機能があります。
制作をしていく中で、これらを実装していくといくつかバグが出てきますが毎回どこが原因がデバッグをするのはとても手間だと感じていました。 特に NPCとの会話は 数十体いるNPCと会話するのは、数時間に及ぶものになっています。
そこで本チームではNPCとの会話におけるデバッグの一部のテスト環境の構築と自動化できる環境を作成を行いました。

www.youtube.com

制作環境

(内容に関わるもののみ記載)

  • Unity 2021.3.0f1

  • Fungus v3.1.9

  • UniTask
  • UniRx

対象者

  • UnityでFungusを使った大きめの(*1) ゲーム制作をしている方
  • テストコードやC#をある程度理解している方

(*1) : 人によって感覚が違いますが、ここではテストコードを書いたほうがプロジェクトの生産性が上がる規模としています。

本プロジェクトにおいての会話実装及び仕様説明

本プロジェクトでは、NPCとの会話に 会話アセットして有名な Fungus を使用して NPCとの会話機能を実装しています。 Fungusは、以下の感じで Insptorから会話内容を確認できたり、ノードベースで会話内容の分岐やループ、終了を作成することができます。

f:id:ayousanz:20220416112737p:plain

会話パートの説明

NPCとの会話では以下の順序で処理を行っています。

  1. プレイヤーが NPCに近づき 特定のキーを押して話しかけるトリガーをONにする。
  2. 目の前にいる NPCから 会話用 Interfaceを GetComponentして、会話処理を開始する(NPC側の会話処理を始める)
  3. NPC側にある会話処理が終了するまで、ほかの処理を待機する。
  4. 会話処理が終了するとプレイヤー側に処理を戻して、ほかの処理を開始する。

制作時に起きた問題点

Fungusを使用して会話を実装する場合、以下のような運用ルールが必ず必要になると思います。

1. flowchartのMessageとスクリプト側のFlowchart用 Message Stringが一致しない問題

Fungus側のMessgeを呼ぶときには、FlowChart.SendFungusMessage(message); を呼び出す必要があります。
このとき message が 呼び出すオブジェクトにアタッチされている Flowchart内の Messageと一致していないと呼び出すことができません

f:id:ayousanz:20220416114116p:plain

こちらのstring値は、作業者が設定するためどうしてもミスが起きてしまうことがあります。

2. Fungus内の変数とスクリプトから呼び際に変数名の一致

Fungusにおいて、会話内の分岐や変数の扱いは variables という flowchart内変数を用いて実装します。

f:id:ayousanz:20220416123928p:plain

この変数をスクリプト側から参照したり変更したりするときは、スクリプト側から変数名を string値で指定する必要性があります。

(以下例 : 常に変数を監視して、会話処理を行っている)

        private void ObservationTalk()
        {
            FlowChart.ObserveEveryValueChanged(value => value.GetBooleanVariable(TalkEventValueName))
                .Skip(1)
                .Where(_ => IsMissionEvent())
                .Subscribe(isTalk => { IsTalk = isTalk; }).AddTo(this);
        }

このときにスクリプト側の変数名と flowchart内の変数名が違っている場合は、処理ができずバグの原因になってしまいます。

こちらも作業者がスクリプト側及びflowchart内のどちらとも設定するため、ミスが起きることが考えられます。

Flowchartでのテストについて

上記の問題点について以下のテストコードで自動化を行いました。
また PRを出した際に GitHub Actionsを用いて CI で自動テストをすることにより、プロジェクトの品質を保つことができています。

(前提として、本プロジェクトでは NPC周りは NPCのBaseクラスを各NPCで使用していたり継承したりしています。以下NPC周りのクラス図)

f:id:ayousanz:20220416125114p:plain

またテストを実行する際に 会話処理がシーンごとにテストを行っています。    テストを実行する際には、以下のコードのようにテスト実行後シーン遷移が終わったのちにヒエラルキー上に存在するNPCを取得して、各テストの引数に渡しています。

        [UnityTest]
        [Category("FungusValuesTest")]
        public IEnumerator NPCFungusValuesInGame()
        {
            SceneManager.LoadSceneAsync("_DreamIsland/_Scenes/NPCs").completed += _ => { NPCFlowchartValueTest(); };
            yield return null;
        }

flowchart内の必須変数の存在テスト

スクリプト側で設定したflowchart内の変数が存在しているかどうかについては、Fungus/Flowchart にある HasVariable を使用しています。

        private static void NPCControllerFlowchartValueTest(IEnumerable<GameObject> npcList)
        {
            foreach (var npc in npcList)
            {
                if (!npc.TryGetComponent<NPCController>(out _)) continue;
                if (!npc.TryGetComponent<Flowchart>(out var flowchart)) continue;
                var isTalkValue = flowchart.HasVariable("isTalk");
                var isPhaseValue = flowchart.HasVariable("phase");
                Assert.True(isTalkValue);
                Assert.True(isPhaseValue);
            }
        }

スクリプトのmessageとflowchart側のmessageの文字列の一致テスト

スクリプト側から FlowchartのMessageBlockを呼び出す際の文字一致に関しては、Fungus/MessageReceivedGetSummary を使用しています。

using System.Linq;
using Fungus;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;

namespace _DreamIsland.Editor
{
    public class SetFlowchartMessage : EditorWindow
    {
        [MenuItem("DreamIsland/SetFlowchartMessage")]
        private static void SetMessage()
        {
            var allObject = Resources.FindObjectsOfTypeAll(typeof(GameObject))
                .Select(c => c as GameObject)
                .Where(c => GetFullPath(c).Contains("NPCs"));
            foreach (var obj in allObject)
            {
                if (!obj.TryGetComponent<Flowchart>(out _)) continue;
                var message = obj.GetComponent<MessageReceived>();
                if (!message.GetSummary().Equals(obj.name))
                {
                    Debug.Log($"{obj.name}のflowchartのmessageを{message.GetSummary()}から{obj.name}に変更します");
                    message.SetMessage(obj.name);
                }
            }
        }

        private static string GetFullPath(GameObject obj)
        {
            return GetFullPath(obj.transform);
        }

        private static string GetFullPath(Transform t)
        {
            var path = t.name;
            var parent = t.parent;
            while (parent)
            {
                path = $"{parent.name}/{path}";
                parent = parent.parent;
            }

            return path;
        }
    }
}
#endif

また、このテストで失敗した際に 以下のような Editor拡張を使用して一括で プロジェクト内にある NPC Prefabを一括で正しいもの変更するようにしています。

f:id:ayousanz:20220416133202p:plain

        private static void IsFlowchartMessageMatchNPCNameTest(IEnumerable<GameObject> npcList)
        {
            foreach (var npc in npcList)
            {
                if (!npc.TryGetComponent<Flowchart>(out _)) continue;
                var messageReceived = npc.GetComponent<MessageReceived>();
                Assert.AreEqual(npc.name, messageReceived.GetSummary());
            }
        }

GitHub Actionsを用いた CI/CD環境構築方法

ayousanz.hatenadiary.jp

GitHub ActionsのTest Reuslt画面では以下のように確認できます。

f:id:ayousanz:20220416132043p:plain

まとめ

テスト環境を作成したこととテストを自動化することによって、リリース前にバグを心配する部分をだいぶ減らすことができました。
また日々の開発でも会話周りの原因をテストができる環境により、原因の解明を迅速の行うことができています。

みなさん テストを頑張って書きましょう! (*注)

(*注) : 個人開発の場合、テストを書くコストに見合わない場合も結構あります。。。

DreamIslandが近々 SteamにReleaseされるため、宣伝及びプレイをお願い致します!

f:id:ayousanz:20220416133559p:plain