【C#】Unity 2Dでできるだけ簡単にアドベンチャーゲーム(ノベルゲーム)を作る その2

kanchi0914.hatenablog.com

前回の記事の続きです。前回の記事では、クリックでテキスト送りの部分まで実装しましたが、当然それだけではゲームとして成立しません。今回は選択肢を表示させ、コマンドを実行することで最低限ゲームとして遊べるようにします。完成すると下のようになります。

f:id:Kanchi0914:20190723065603g:plain

まずはコマンドを選択するため、動的にコンポーネントの数が変化するPanelを作ります。HierarchyビューのCanvasに適当にPanelを配置し、PanelのInspectorビューからVertical Layout Groupを追加、以下のように設定します。なお今回脱出ゲームっぽい感じにしたかったので、それにあわせて背景も変えてあります。 f:id:Kanchi0914:20190723064530p:plain f:id:Kanchi0914:20190723064326p:plain

次に、今用意したボタン用のPanelに子要素としてButtonを追加し、ButtonのInspectorビューからLayout Elementを追加、Min Heightの値を適当に設定します。 f:id:Kanchi0914:20190723064420p:plain

設定した後、ButtonをAssetsフォルダにドラッグ&ドロップし、Buttonをプレハブ化します。 f:id:Kanchi0914:20190723064546p:plain

 これでUnityエディタ側の準備は完了です。では次にスクリプトを用意していきましょう。
 まずは以下のようにコマンド選択のためのOptionクラスを用意します。

    class Option
    {
        public string Text;
        public Action Action;
        public Func<bool> IsFlagOK = () => { return true; };
    }

 ここでActionFuncというのは、デリゲートと呼ばれる型の一種です。詳しい説明は省きますが、メソッドの参照をそのまま渡せる型だと思ってください。つまり、これを使ってさっき作ったボタンにメソッドの参照を渡すことで、ボタンでコマンドを選択→特定の処理を実行、という流れができるわけですね。  またScenarioクラスに、このOptionクラスのListを追加したりするなど修正を加えます。

    class Scenario
    {
        public string ScenarioID;
        public List<string> Texts = new List<string>();
        public List<Option> Options = new List<Option>();
        public string NextScenarioID;
    }

実際にはどう使うかというと、ScenarioクラスのOptionsに選ばせたいコマンドの数だけOptionクラスのインスタンスを追加し、Textにコマンドの内容、Actionにそのコマンドを実行した際の処理となるメソッドへの参照を代入します。

        scenario02 = new Scenario()
        {
            ScenarioID = "scenario02",
            Texts = new List<string>()
            {
                "どうする?",
            },
            Options = new List<Option>
            {
                new Option()
                {
                    Text = "辺りを見渡す",
                    Action = LookAround
                },
                new Option()
                {
                    Text = "鍵を拾う",
                    Action = TakeKey,
                    IsFlagOK = () =>
                    {
                        return isCheckedKey && !items.Contains("Key");
                    }
                },
                new Option()
                {
                    Text = "扉を開ける",
                    Action = OpenDoor,
                }
            }

        };
    public void LookAround()
    {
        var scenario = new Scenario();
        scenario.NextScenarioID = "scenario02";
        if (!items.Contains("Key"))
        {
            scenario.Texts.Add("足元に鍵が落ちている");
            isCheckedKey = true;
        }
        else
        {
            scenario.Texts.Add("足元には何もない");
        }
        SetScenario(scenario);
    }

    public void OpenDoor()
    {
        var scenario = new Scenario();
        if (items.Contains("Key"))
        {
            scenario.Texts.Add("鍵を使って扉を開いた");
            scenario.Texts.Add("クリアー!");
        }
        else
        {
            scenario.Texts.Add("鍵がかかっていて開かない");
            scenario.NextScenarioID = "scenario02";
        }
        SetScenario(scenario);
    }

    public void TakeKey()
    {
        var scenario = new Scenario();
        scenario.Texts.Add("鍵を拾った");
        scenario.NextScenarioID = "scenario02";
        SetScenario(scenario);
        items.Add("Key");
    }

またIsFlagOKは選択肢を出現させるかどうかのbool値で、基本的にはTrueをとりますが、これを特定の条件を満たしているかどうかで分けることができます。 ここでは、「”辺りを見渡す”コマンドを選択した」&「まだアイテム”Key”を拾っていない」状態でのみ、「鍵を拾う」コマンドが出現するようになっています。

                new Option()
                {
                    Text = "鍵を拾う",
                    Action = TakeKey,
                    IsFlagOK = () =>
                    {
                        return isCheckedKey && !items.Contains("Key");
                    }
                },

IsFlagOKの右辺のような書き方をラムダ式と呼び、簡単に言えばメソッドの宣言を省略して書くことが出来ます。 これと先程のデリゲートを組み合わせることで、かなり自由にコードを書くことができるようになります。

あとはコマンドに応じて先程プレハブ化したボタンを設定する処理を書いたりします。

    void SetOptions()
    {
        foreach (Option o in currentScenario.Options)
        {
            if (o.IsFlagOK())
            {
                Button b = Instantiate(optionButton);
                Text text = b.GetComponentInChildren<Text>();
                text.text = o.Text;
                b.onClick.AddListener(() => o.Action());
                b.onClick.AddListener(() => ClearButtons());
                b.transform.SetParent(buttonPanel, false);
            }
        }
    }

buttonPaneloptionButonは先程用意したパネルとボタンへの参照です。前回のText同様GameControllerのInspector上でアタッチしておきます。このように、Unityチュートリアルなどで見るボタンの処理には、先程のデリゲートとラムダ式が用いられていたわけですね。 その他色々修正したコード全体は以下に上げておきます。

https://github.com/kanchi0914/AdventureGameSample/

これで、コマンド選択の結果によってフラグの設定を切り替えたり、あるいはその内容に応じてコマンドを変更したりすることができるようになりました。 とりあえずここまでできれば、簡単なノベルゲーム程度なら作れるようになったのではないでしょうか。本スクリプトではcurrentScenarioにScenarioクラスのオブジェクトをセットすることでメッセージが表示されるため、例えば画面上の特定の点をクリックしたときにメッセージや選択肢を表示させたりすることで、脱出ゲーム的なのも作れると思います。

今回は処理を行うタイミングがコマンドを実行した場合に限られましたが、例えばC# ver7.0から導入されたタプル機能を使い、ScenarioクラスのTextsを以下のように変更することで、テキストを一行送る度に処理を行うことができるようになります。なおUnityでC# 7.0を使えるのは特定のバージョン以降に限られます(多分)。

public List<(string, Action)> Texts = new List<(string, Action)>();

こうすることで、例えばメッセージに併せてキャラクターの画像を変更できたりという、いわばギャルゲーっぽい処理を簡単に実装することができます。これについては今後時間があれば書きたいと思います。