🎵

【Unity】ScenarioFlowによるシナリオ実装#2-2(TaskFlowの構造)

2023/05/06に公開

はじめに

こんにちは.伊都アキラです.
前回の記事では,TaskFlowのセットアップを行い,非同期のScenarioMethod(ScenarioTask)を呼び出しました.そして,ScenarioTaskはキャンセルすることができることも確認しました.

今回の記事では,TaskFlowで提供されているインターフェース及びクラスの関係について解説していきます.

TaskFlowの構造

TaskFlowを利用するにあたって,重要なのはIScenarioBookReaderインターフェースと,ICancellationDecoderインターフェースです.TaskFlowを使うために,これら2つのインターフェースをそれぞれ実装するインスタンスを取得することが目標となります.

ここからはそのために,どのクラスを使えば良いか,それぞれのコンストラクタにどのようなクラスを渡せば良いかを説明していきます.

また,今回の解説ではクラス図をいくつか掲載しています.こちらから全体図を閲覧できるので,図が小さくて見にくいという方はぜひご覧ください.

IScenarioBookReader周辺の依存関係


IScenarioBookReader周辺の依存関係

上に示すのが,IScenarioBookReader周辺の依存関係についてまとめたクラス図になります.
順にクラス及びインターフェースを見ていきましょう.

IScenarioBookReaderインターフェース

ScenarioBookを受け取り,シナリオ進行を管理するインターフェースです.メンバのUniTask ReadScenarioBookAsync(IScenarioBook, CancellationToken)IScenarioBookインターフェースを渡すことで,自動的にScenarioBook中のScenarioMethodを実行してくれます.

ScenarioBookReaderクラス

IScenarioBookReaderインターフェースを実装するクラスです.また,他にはScenarioBook中の任意のラベルが付加された場所を開くための,ILabelOpenerインターフェースを実装しています.
ソースファイルにシナリオを記述する際はScenarioMethodにラベルを付加し,IScenarioBookOpenLabelによってその場所を開くことができましたが,このインターフェースを利用することで,シナリオ進行中に柔軟にラベルを行き来することができるようになります.

コンストラクタには,IScenarioTaskExecuterインターフェースを要求します.

IScenarioTaskExecuterインターフェース

ScenarioBook中のScenarioMethodを実行する際,ScenarioMethodが同期処理であればそのまま実行するだけです.しかし,ScenarioMethodの返り値がUniTask,つまり非同期処理である場合に,どのように実行・キャンセルを行うかを管理する必要があります.

このインターフェースは,そのような責務を負っています.
UniTask ExecuteScenarioTaskAsync(UniTask, CancellationToken)にScenarioTaskを渡すことによって,複数のScenarioTaskの並列実行,キャンセル処理など,柔軟な実行が可能になります.

ScenarioTaskExecuterTokenCodeDecorator

IScenarioTaskExecuterインターフェースを実装しています.

クラス名にTokenCodeという名前が入っていますが,これはどのようにScenarioTaskを実行するかを指定するためのものです.

前回,ScenarioTaskの呼び出しのために,ソースファイルは以下のように書いていました.


前回のソースファイル

どちらのScenarioTaskも,3つ目のパラメータがCancellationTokenに対応していますが,ここではstdと記述されています.後の記事で詳しく解説しますが,これは通常通りにScenarioTaskを実行するという意味になります.

すなわち,ScenarioTaskのパラメータの内,CancellationTokenに対応するものをトークンコードと呼んでいるのであり,前回作成したCancellationToken用のDecoderがそれを解釈することによって,様々な方法でScenarioTaskが実行されるわけです.

コンストラクタでは,INextProgressorインターフェースIScenarioTaskExecuterインターフェース,そしてITokenCodeGetterインターフェースを要求します.

INextProgressorは前回に解説した通り,「次のシナリオへ進む」タイミングを調整するためのものですが,IScenarioTaskExecuterを要求するのは,あらゆるトークンコードに対応するためです.

TaskFlowで使用できる,"std"を含めた特殊なトークンコードは限られた数しかありません.それら以外のトークンコードをパラメータとして渡した場合,ScenarioTaskをどう実行するかはコンストラクタに渡した他のIScenarioTaskExecuterに委ねられます.基本的には下のScenarioTaskExecuterを,機能を拡張する場合には別のクラスを渡すことになるでしょう.

最後にITokenCodeGetterですが,これはScenarioTaskにどのようなトークンコードが指定されたかを知るためのものです.

ScenarioTaskExecuter

IScenarioTaskExecuterインターフェースを実装しています.

前述のScenarioTaskExecuterTokenCodeDecoratorと同様に,ScenarioTaskの実行の仕方を管理している点は同様です.ただし,あらゆるトークンコードが許容されるという点で異なります.

このクラスでは,すべてのScenarioTaskは指定のトークンコードに紐づけられて,ディクショナリで保管されます.
そして,このクラスが実装しているIScenarioTaskAccepterインターフェースのメンバであるUniTask AcceptScenarioTaskAsync(string)により,保管されたScenarioTaskの終了を待つことができます.

例として,トークンコードに"TaskA"を渡したScenarioTaskを実行した場合,AcceptScenarioTask("TaskA")を呼び出すことで,そのScenarioTaskを取得することができます.後はそれをawaitすれば良いわけですね.

コンストラクタには,ITokenCodeGetterインターフェースを持ちます.

ICancellationTokenDecoder周辺の依存関係


ICancellationTokenDecoder周辺の依存関係

上に,ITokenCodeDecoder周辺の依存関係をまとめたクラス図を示しています.
IScenarioBookReader同様,順にクラスとインターフェースを見ていきましょう.

ICancellationTokenDecoderインターフェース

CancellationToken用のDecoderを実装するためのインターフェースです.

非同期処理のキャンセルには,CancellationTokenを使用しますから,ScenarioTaskの引数としてもその型の使用が欠かせません.TaskFlow自体は対応するDecoderを提供しませんが,このインターフェースを実装するクラスを提供しており,実質的にDecoderとして求められる処理がそこに集約しています.

メンバとしては,文字列をCancellationTokenに変換するためのCancellationToken Decode(string)を持ちます.

CancellationTokenDecoderTokenCodeDecoratorクラス

ICancellationTokenDecoderインターフェースを実装しています.

IScenarioTaskExecuterと同じように,「どのようにScenarioTaskを実行するか」を管理するクラスですが,このクラスは特にキャンセル処理の方法を管理します.

そして,クラス名にTokenCodeとあるように,このクラスではScenarioTaskに渡されたトークンコードによって好きなキャンセル処理の仕様を選ぶことができます.例えば,前回使用していたトークンコード,stdの場合は単にキャンセル可能となりますが,他のトークンコードではキャンセル不可となるものもあります.


前回のソースファイル

コンストラクタにはICancellationTokenDecoderインターフェースICancellationProgressor,そしてITokenCodeSetterを要求しています.

まず,ICancellationTokenDecoderについて,このクラスにおいて定義されている特別なトークンコード以外がScenarioTaskに渡された場合は,ここでコンストラクタに渡された他のICancellationTokenDecoderに処理が委ねられます.基本的には下のCancellationTokenDecoderクラスを,機能を拡張したい場合は別のクラスを渡すことになるでしょう.

そして,ICancellationProgressorについては前回に解説した通り,ScenarioTaskをキャンセルする方法を提供するものです.

最後に,ITokenCodeSetterは,ScenarioTaskに指定されたトークンコードを伝えるためのものです.ソースファイルで指定されたトークンコードはDecoderに渡され,このクラスに渡され,そしてITokenCodeSetterITokenCodeGetterを通してScenarioTaskExecuterらに渡されるわけです.

CancellationTokenDecoderクラス

ICancellationTokenDecoderインターフェースを実装しています.

このクラスでは,CancellationTokenDecoderTokenCodeDecoratorクラスとは異なり,あらゆるトークンコードCancellationTokenに変換することが可能です.

同様,ScenarioTaskに渡されたトークンコードとCancellationTokenを生成するためのCancellationTokenSourceが紐づけられ,後から呼び出し時に使用したトークンコードによってScenarioTaskのキャンセルが可能になります.

このクラスにおけるScenarioTaskのキャンセルには,このクラスが実装するICancellation
TokenCancelerインターフェース
のメンバ,void Cancel(string) を利用します.例えば,トークンコード"TaskA"でScenarioTaskを呼び出した場合,Cancel("TaskA")でそのScenarioTaskをキャンセルすることができます.

コンストラクタでは,ITokenCodeSetterインターフェースを要求します.

CancellationProgressorTokenCodeDecoratorクラス

ICancellationProgressorインターフェースを実装しています.

このクラスは,ScenarioTaskのキャンセル処理の管理を行います.その方法は,後の記事で解説するトークンコードによって異なります.

コンストラクタではICancellationProgressorインターフェースを要求しており,TaskFlowの外部で実装する必要があります.実際のキャンセルを引き起こすトリガーをどのようなものにするのかは,それに任せることになります.

IDisposableインターフェース

CancellationTokenDecoderクラス及びCancellationTokenDecoderTokenCodeDecoratorクラスは,IDisposableインターフェースを実装するCancellationTokenSourceを利用します.
そのため,これら2つのインターフェースもIDisposableインターフェースを実装しており,クラスを使い終わったらDisposeを呼び出す必要があります(呼び出しを忘れても,まず問題は起きませんが).

TokenCodeHolder


TokenCodeHolder

これまでに出てきたITokenCodeGetterITokenCodeSetterについてですが,これらを実装するクラスをTaskFlowはTokenCodeHolderとして提供しています.

今までに出てきたクラスでITokenCodeGetterITokenCdeSetterを要求するものについてはこのクラスのインスタンスを渡してやれば良いわけですが,どのクラスにも共通のインスタンスを渡さなければならないことに注意しましょう.インスタンスを複数作成してそれぞれを別のクラスに渡した場合,トークンコードの伝達が適切に行われなくなってしまいます.

良い例

        //インスタンスは一つだけ作る
        TokenCodeHolder tokenCodeHolder = new TokenCodeHolder();
        ScenarioTaskExecuter scenarioTaskExecuter = new ScenarioTaskExecuter(tokenCodeHolder);
        CancellationTokenDecoder cancellationTokenDecoder = new CancellationTokenDecoder(tokenCodeHolder);

悪い例

        //別々のインスタンスを渡している
        ScenarioTaskExecuter scenarioTaskExecuter = new ScenarioTaskExecuter(new TokenCodeHolder());
        CancellationTokenDecoder cancellationTokenDecoder = new CancellationTokenDecoder(new TokenCodeHolder());

Progressorについて

ScenarioTaskのシナリオ進行及びキャンセルのタイミングを管理するためのINextProgressorICancellationProgressorについてですが,TaskFlowがデフォルトで提供するクラスはありません(CancellationProgressorTokenCodeDecoratorはあくまで補助目的).

しかし,今後に行う予定のサンプルゲームの作成では,様々なProgressorを実装します.そこで,Progressorの実装の容易さと,拡張性の高さ,表現力の高さをお分かりいただけると思います.

サンプルゲームでは,例えば,「キー入力」「ボタンの押下」「オート進行」「スキップ進行」などをトリガーとするProgressorを実装しています.

おわりに

今回の記事では,TaskFlowが提供するインターフェース及びクラスの構造,各役割について解説しました.最後に,前回に実装したTaskFlowのセットアップの一例を再掲しておきます.

        TokenCodeHolder tokenCodeHolder = new TokenCodeHolder();

        SpaceKeyProgressor spaceKeyProgressor = new SpaceKeyProgressor();

        IScenarioTaskExecuter scenarioTaskExecuter = new ScenarioTaskExecuterTokenCodeDecorator(
            new ScenarioTaskExecuter(tokenCodeHolder),
            spaceKeyProgressor,
            tokenCodeHolder);
        
        IScenarioBookReader scenarioBookReader = new ScenarioBookReader(scenarioTaskExecuter);

        ICancellationTokenDecoder cancellationTokenDecoder = new CancellationTokenDecoderTokenCodeDecorator(
            new CancellationTokenDecoder(tokenCodeHolder),
            new CancellationProgressorTokenCodeDecorator(spaceKeyProgressor, tokenCodeHolder),
            tokenCodeHolder);

クラス図で各クラスとインターフェースの関係を理解し,各役割を知ることで,このコードの意味が理解しやすくなったと思います.

次回は,TaskFlowで使用できる特別なトークンコードについて,その種類と,各トークンコードによりScenarioTaskがどのように実行されるかを解説していきます.

最後までお読みいただき,ありがとうございました.

Discussion