🎼

【Unity】ScenarioFlowによるシナリオ実装#1-3(Hello, world!の表示)

2023/04/12に公開

はじめに

こんにちは.伊都アキラです.
前回の記事では,ScenarioFlowの思想について,すなわち長所についてお話ししました.

今回の記事からは,実際にScenarioFlowを動かし,その使い方を解説していきます.

目標

今回の目標は,ずばりHello, world!の出力です.
これは大抵のプログラミング言語の入門書で初めに書くことになるプログラムですが,ScenarioFlowもそこから始めたいと思います.

具体的には,任意のメッセージを出力するコマンドを実装し,ScenarioFlowで引数をHello, world!として実行します.
この作業を通して,ScenarioFlowでコマンドを追加して,それを実行するまでの流れをつかんでいきましょう.

ScenarioFlowの導入

まずは,ScenarioFlowをUnityにインポートしましょう.
Unityで適当なプロジェクトを立ち上げたら,こちらのURL(ScenarioFlow)から.unitypackageファイルをダウンロードし,Unityへインポートしてください(私のUnityのバージョンは2021.3.22f1です).

すると,おそらく次のようなエラーが出ると思います.


ScenarioFlowインポート直後のエラーメッセージ

これは,ScenarioFlowの標準拡張モジュールであるTaskFlowが,UniTaskに依存しているためです.こちらのURL(UniTask)から,UniTaskもインポートしてください(この時点で最新のバージョン2.3.3をインポートしています).

UniTaskのインポートにより,エラーメッセージは消えたかと思います.
これでScenarioFlowの導入は完了です.

Hello, world!の表示

ScenarioFlowの導入が済んだら,以下の手順を踏んで,ScenarioFlowでHello, world!を表示していきます.

  1. ScenarioMethodの追加
  2. Decoderの追加
  3. ソースファイルの準備
  4. ScenarioMethodの実行

ScenarioMethodの追加

まずは,ScenarioMethodを追加する方法を解説します.
まずは以下のようなコードを実装してみましょう.

MessageLogger.cs
using ScenarioFlow;
using UnityEngine;

public class MessageLogger : IReflectable
{
    [ScenarioMethod("message")]
    public void LogMessage(string message)
    {
        Debug.Log(message);
    }
}

これが,「任意の文字列をデバックコンソールに出力するコマンド」を実装しているクラスになります.
重要なポイントを見ていきましょう.

ScenarioMethod属性の付加

前提として,ScenarioFlowはリフレクションを利用してテキストからメソッドを直接呼び出します.ですから,クラスで宣言されたメソッドがそのままコマンドになるわけです.

前回の記事で,ScenarioFlowはコマンドの追加が簡単であるということをアピールしましたが,それを実現することができる大きな理由がこれです.
ScenarioFlowでは,コマンドの処理を「コマンドである」と意識することなく,普段メソッドを記述するように,自然に書くことができるのです.

さて,ScenarioFlowが「どのメソッドを呼び出して良いか」を判別するため,コマンドとして呼び出したいメソッドにはScenarioMethod属性を付加する必要があります.そして,当然ですが,メソッドのアクセス修飾子はpublicでなければなりません.

上のコードでは[ScenarioMethod("message")]を付加していますが,この場合,"message"という名前のコマンドとして,LogMessageメソッドを登録することになります.
また,このコマンドはstring型の引数を一つ持ち,属性を付加したメソッドとパラメータは一致することになります.

IReflectable interfaceの実装

ScenarioMethodを持つクラスはすべて,IReflectableインターフェースを実装している必要があります.
ただし,IReflectable空のインターフェースであり,実装しなければならないメンバーはありません

今回は詳しくは解説しませんが,このような仕様になっている理由は,ScenarioFlowをDI-friendlyにするためです.

Decoderの追加

ScenarioFlowはリフレクションを利用してScenarioMethodを呼び出しますが,それには一つ,問題があります.
それは,シナリオのデータ(コマンドを呼び出す順番)が記述されるソースファイルはテキストベースなので,C#はstring型のパラメータしか受け取ることができないということです.そして,そのために文字列から適切な型への変換を行う必要があります.

しかし,前回の記事で指摘したように,各ScenarioMethod内で必要な型変換を行うのはナンセンスです.型変換の処理は1つの場所に記述されているべきですし,各ScenarioMethodが型変換の方法を気にするべきでもありません.

ScenarioFlowでは,この問題を解決するため,このような型変換はすべてDecoderで行うことになっています.

ここでのDecoderとは,ある文字列をある型に変換をする関数です.ScenarioFlowがパラメータごとに適切なDecoderを選択して使ってくれるので,ScenarioMethodを定義するときには型変換のことを気にせず,普段メソッドを書く時と同じように書くことができるのです.

すなわち,ScenarioMethodの引数として使いたい型について,それぞれに対応するDecoderが必要です.

ただし,ScenarioFlowはDecoderを標準で提供しないので,すべてScenarioFlowの外で実装しなければなりません.これは,どのような型が使われるかが分からないからでもありますが,変換の方法に自由度を持たせるためでもあります.

さて,前に定義したScenarioMethodである"message"は,string型の引数を持っています.そのため,string用のDecoderを実装しなければなりません.
引数がstring型の場合,特別な変換処理は必要ありませんが,すべての型は同等に扱われるので,string型にもDecoderを用意する必要があります.

以下のように,string型の引数を一つ持ち,string型の返値を持つ関数にDecoder属性を付加しましょう.

PrimitiveDecoder.cs
using ScenarioFlow;

public class PrimitiveDecoder : IReflectable
{
    [Decoder]
    public string StringDecoder(string source)
    {
        return source;
    }
}

このとき,以下の点に注意してください.

  • Decoderはstring型の引数を一つだけ持たなければならない
  • Decoderのアクセス修飾子はpublicでなければならない
  • Decoderの返値の型が,SceanrioMethodの引数としての使用を許可される
  • Decoderを宣言しているクラスはIReflectableインターフェースを実装しなければならない

ソースファイルの準備

ScenarioMethodの準備が整ったので,次はソースファイルにシナリオ(呼び出すScenarioMethodと,その順番)を記述していきましょう.

ソースファイルの種類として,ScenarioFlowは標準でテキストファイル(.txt)を,拡張モジュールであるExcelFlowではエクセルファイル(.xlsx)を提供しています.
テキストファイルは一応使うことはできますが,エクセルファイルの方が明らかに扱いやすいので,エクセルファイルの方をソースファイルとして使用することにしましょう.

エクセルファイルは,デフォルトではUnityで使用することが困難です.
DefaultAssetとしてインポートされ,非常に扱いにくい状態になります.


通常のUnityでの.xlsxファイルの状態

しかし,ExcelFlowがインポートされた状態であれば,以下のようにExcelAssetとして認識されます.


ExcelFlowインポート後の.xlsxファイルの状態

ScenarioFlowで使用する限りは,ExcelAssetのデータ構造について知っている必要はありません.

ただ,軽く触れておくと,ExcelAssetはエクセルファイル内のすべてのセルに入っている文字列を配列として保持しているだけの非常に単純な構造をしています.シリアル化にも対応しており,オブジェクトにアタッチすることができるのでUnity内で扱いやすくなっています.

さて,Unityに適当なエクセルファイルをインポートし,以下のように記述しましょう.

文法はとても簡単です.以下の規則を守るようにしましょう.

  • 一番左の列に呼び出したいScenarioMethodの名前を書く
  • 左から2列目以降は呼び出すScenarioMethodの引数を書く
  • 有効な記述の範囲を<Page>と</Page>で閉じる(これらは1番左の列に書く)

これで,上に書かれたScenarioMethodから順に呼び出されます.
なお,「有効な記述の範囲を<Page>と</Page>で閉じる」と説明していますが,その外に記述されている内容は一切読み取られません.ですから,それらのシンボルで閉じられていない領域は,コメントやメモを残すのに適していると思います.

また,エクセルファイルであれば色を変えたり,一番上のセルを固定したりできますね.
これらの操作もインポートに一切影響を与えません.

ScenarioMethodの実行

いよいよ,ScenarioMethodを実行し,Hello, world!を出力します.
適当なゲームオブジェクト(GameManagerオブジェクトとします)を配置し,それに以下のコードをアタッチしてください.

GameManager.cs
using ScenarioFlow;
using ScenarioFlow.ExcelFlow;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    //読み込むシナリオのソースファイル
    [SerializeField]
    private ExcelAsset excelAsset;

    private void Start()
    {
        //ScenarioMethodをExcelScenarioPublisherに提供する
        IScenarioMethodSearcher scenarioMethodSearcher =
            new ScenarioMethodSeacher(
                new IReflectable[]
                {
                    //使用したいScenarioMethodとDecoderを宣言している
                    //クラスのインスタンス
                    new MessageLogger(),
                    new PrimitiveDecoder(),
                });
        //ExcelAssetをScenarioBookに変換する
        IScenarioPublisher<ExcelAsset> excelScenarioPublisher = 
            new ExcelScenarioPublisher(scenarioMethodSearcher);
        //ExcelAsset -> ScenarioBook
        IScenarioBook scenarioBook = excelScenarioPublisher.Publish(excelAsset);
        //ScenarioMethodをすべて実行
        ReadScenarioBook(scenarioBook);
    }

    //ScenarioBook中のScenarioMethodをすべて実行
    private void ReadScenarioBook(IScenarioBook scenarioBook)
    {
        foreach(IScenarioPage scenarioPage in scenarioBook.ReadAll())
        {
            foreach(IScenarioSentence scenarioSentence in scenarioPage.ReadAll())
            {
                scenarioSentence.OnRead();
            }
        }
    }
}

そして,GameManagerオブジェクトに,シナリオのソースファイルであるエクセルファイルをアタッチしてください.


GameManagerオブジェクトに.xlsxファイルをアタッチ

この状態でUnityを実行すると,デバッグコンソールにHello, world!が出力されるはずです.


Unityの実行結果

では,重要なポイントを見ていきましょう.

IScenarioBook

シナリオのソースファイルに書かれた,呼び出すべきScenarioMethodとその順番を保持したオブジェクトをScenarioBookと呼んでいます.
そして,そのScenarioBookに必要な機能を,IScenarioBookインターフェースが提供しています.

詳細な使い方は次回解説しますが,構造としては実際の本と同じようなものです.
IScenarioBookIScenarioPageを持ち,IScenarioPageIScenarioSentenceを持ちます.そして,IScenarioSentenceOnReadを呼び出すと,それに対応するScenarioMethodが呼び出されます.

IScenarioPublisher

IScenarioPublisher<TSource>は,TSource型のオブジェクトをIScenarioBookに変換するPublish(TSource source)を提供するジェネリックインターフェースです.
ExcelFlowはIScenarioPublisher<ExcelAsset>を実装するExcelScenarioPublisherを提供しており,これによってExcelAssetIScenarioBookに変換することができます.

なお,今回は使用していませんが,ScenarioFlowはIScenarioPublisher<TextAsset>を実装するTsvScenarioPublisherを提供しており,これによってTextAssetIScenarioBookに変換することができます.

IScenarioMethodSeacher

ScenarioMethodの情報を提供するインターフェースです.
ScenarioFlowはこれを実装するScenarioMethodSearcherクラスを提供していますが,実使用上は構造を把握している必要はありません.

前述のExcelScenarioPublisherのインスタンスを作成する際,そのコンストラクタにIScenarioMethodSearcherを渡す必要があるので,それを実装しているScenarioMethodSearcherのインスタンスを生成し,渡さなければなりません.
その際,ScenarioMethodSearcherのコンストラクタには,IReflectableインターフェースの配列を渡すことになっています.その配列の中には,使用したいScenarioMethod及びDecoderを宣言しているクラスのインスタンスを含めるようにしましょう.

おわりに

今回の記事では,ScenarioFlowを利用してHello, world!を出力する方法を解説しました.

次回は,ScenarioBookの構造と使い方について解説します.
最後までお読みいただきありがとうございました。

Discussion