🎵

【Unity】ScenarioFlowによるシナリオ実装#2-3(トークンコードの性質)

2023/05/10に公開

はじめに

こんにちは.伊都アキラです.

前回の記事では,TaskFlow内で提供されているクラスやインターフェースの関係について解説しました.そこに,新たな概念としてトークンコードというものが出てきました.

今回の記事では,そのトークンコードの種類や性質について解説していきます.

トークンコードの概要

トークンコードとは

トークンコードとは,ソースファイルにScenarioTaskの呼び出しを記述する場合に,CancellationToken型の引数を指定するために書く文字列のことです.

例えば,以下に示すScenarioTaskを呼び出すとき,同時に以下に示しているソースファイル内で指定されているトークンコードは"std"です.

    [ScenarioMethod("message.after", "指定の秒数待機した後,メッセージを表示")]
    public async UniTask LogMessageAfterSecondsAsync(string message, float delaySec, CancellationToken cancellationToken)
    {
        //中略
    }

    [ScenarioMethod("count.sec", "指定された秒数をカウント")]
    public async UniTask CountSecondsAsync(string counterName, int time, CancellationToken cancellationToken)
    {
        //中略
    }


ソースファイル:CancellationToken型の引数に注目

この"std"という文字列がCancellationToken用のDecoderによって,string型からCancellationToken型に変換されるのでした.そして,変換の元となる文字列を,トークンコードと呼んでいます.

まず意識していただきたいことは,ここで指定するトークンコードは何でも良いというわけではなく,どのトークンコードにも意味があるということです.

トークンコードが果たす役割とその存在意義を理解するため,実際にシナリオ進行を行う時のことを考えてみましょう.

トークンコードがなくても良い例

シナリオ進行を行うためには,様々な演出が必要です.キャラクターのセリフを表示したり,キャラクターの絵を表示したりすることはもちろんのこと,音楽を流したり,背景を変更したりと,挙げ始めればキリがありません.
そこで,ScenarioFlowその演出を実現するためには,それぞれの演出に対応するScenarioTask(または非同期でないScenarioMethod)を作成し,ソースファイルから呼び出すことになるわけです.

例えば,「キャラクターのセリフの表示」「キャラクターの絵の表示/変更」のためのScenarioTaskあるいはScenarioMethodを作成したとき,以下のようなソースファイルを用意すれば,問題なくシナリオ進行が行われるでしょう.

上から順番に一つずつScenarioTaskが呼び出されることで,「キャラクターの表示 → セリフの表示」の流れになることは想像がつくかと思います.

このようなシナリオ進行しか行わないのであれば,トークンコードは必要ありません.


トークンコードなしで問題がないシナリオ進行の例

トークンコードがないと困る例

さて,トークンコードがないと困ってしまうような,最も分かりやすい事例を示しましょう.
上から順番に一つずつScenarioTaskが呼び出される,という点がポイントです.


トークンコードがないと問題があるシナリオ進行の例

シナリオ進行の最後に,「飛び跳ねる」という演出を追加しています.これは,画面に表示されているキャラクターがぴょんと飛び跳ねるような演出だと考えていただいて結構です.
そして,当然ですが「飛び跳ねる」はアニメーションですから,非同期のScenarioTaskです.

では,ここに書かれたScenarioTaskを,上から順番に一つずつ実行していくとどうなるでしょうか.

おそらく,想像できたかと思います.アリスの立ち絵が「アリス_笑顔」になり,数秒かかってセリフが表示され,それらの処理が終わった後にアリスがぴょんと飛び跳ねるような,訳の分からない演出になります(少なくとも意図していた動きではありません).

そもそも何を求めていたのかということを考えてみれば,それは,セリフが表示されている間にアリスがぴょんと飛び跳ねるという演出です.
すなわち,「セリフの表示」,そして「飛び跳ねる」という2つの演出を並列に行いたかったわけです.

解決策

これで,問題点が明らかになりました.
上から順番に一つずつScenarioTaskを実行していたのでは,複数のScenarioTaskを同時に実行することができません.

だからといって,「セリフの表示をしつつ,キャラクターが飛び跳ねるアニメーションを実行する」ようなScenarioTaskを作成するのは馬鹿げています.同時に実行したい演出は多岐に渡るでしょうし,一体,豊富な演出を用意するためにどれほどのScenarioTaskを実装しなくてはならなくなるのでしょうか.

やはり,一つのScenarioTaskはできる限り一つの演出を担当するようにしたいものです.そのためには,複数のScenarioTaskの並列実行は必須です.

そこで,トークンコードがその問題を解決します.
トークンコードを指定することで,上から順番に一つずつ実行するだけではなく,より柔軟に実行方法を選択することができます.

実際,先の例で意図していた演出を実現するためには,ソースファイルは以下のように書けます.


トークンコードによる演出の並列実行

詳細は後述しますが,paralは,後ろのScenarioTaskを並列に実行するという意味のトークンコードです.これで,6,7行目の演出が同時に実行されます.

トークンコード一覧

TaskFlowがデフォルトで提供するトークンコードには,すでに出てきた"std"や"paral"の他にも,いくつかの種類があります.
それらを表にまとめたものを,以下に示します.

トークンコード 進行命令 キャンセル命令 説明
std 待つ 標準的な実行方法.
forced 待つ 不可 キャンセルができない.強制実行.
fluent 待たない 処理が完了次第,瞬時に次の処理へ進む.
forced.fluent 待たない 不可 キャンセル不可かつ処理が完了次第,次へ進む.
serial 待たない 処理が完了次第,次の処理へ.キャンセルが連鎖.
paral 待たない 処理開始と同時に次の処理へ.キャンセルが連鎖.
その他 待たない 処理開始と同時に次の処理へ.最も表現力が高い.

進行命令とは,INextProgressorによってコントロールされる,一つの処理が完了した後,次の処理へ進む許可を出す命令です.
そして,キャンセル命令とは,ICancellationProgressorによってコントロールされる,ある処理を実行している間,その処理をキャンセルし即時終了させる命令のことです.

トークンコードの性質

では,各トークンコードを使い分けて,それぞれの性質を確認していきましょう.
使用するScenarioTaskはSimpleCounter.cs内のcount.sec,これをいくつか並べたソースファイルをGameManager.csで動作させていきます.#2-1で実装したコードをそのまま使用してください.

std

stdは,標準的なシナリオ進行を指定します.
一つの処理が完了するごとに進行命令を待ち,また,その処理は途中でキャンセル可能です.


ソースファイル:std

ソースファイルを実行し,以下のことを確かめてください.

  • 上から順に,一つずつカウントが実行される
  • 一つの処理が完了するたび,スペースキーを押すと次に進む
  • 一つの処理が進行中,スペースキーを押すとカウントがキャンセルされる


std(正常): 順にカウントが実行される


std(キャンセル): カウントが中断される

forced

forcedは,キャンセルしてほしくないシナリオ進行に対して指定します.
一つの処理が完了するごとに進行命令を待つのはstdと同じですが,処理はキャンセルできません.


ソースファイル:forced

ソースファイルを実行し,以下のことを確かめてください.

  • 上から順に,一つずつカウントが実行される
  • 一つの処理が完了するたび,スペースキーを押すと次に進む
  • 一つの処理が進行中,スペースキーを押してもカウントは止まらない


forced: キャンセルはできない

fluent

fluentは,進行命令を待たずに次々とシナリオ進行を行いたいときに指定します.
一つの処理が完了した瞬間に次の処理へ進む,流れるようなシナリオ進行になります.


ソースファイル:fluent

ソースファイルを実行し,以下のことを確かめてください.

  • 上から順に,一つずつカウントが実行される
  • 一つの処理が完了するたび,スペースキーを押さなくても次に進む
  • 一つの処理が進行中,スペースキーを押すとカウントがキャンセルされる


fluent(正常): スペースキーを押さなくても処理が続く


fluent(キャンセル): キャンセルした瞬間,次に進む

forced.fluent

forced.fluentは,forcedとfluentを組み合わせたものです.
進行命令を待たずに処理は継続し,またその処理はキャンセルできません.


ソースファイル:forced.fluent

ソースファイルを実行し,以下のことを確かめてください.

  • 上から順に,一つずつカウントが実行される
  • 一つの処理が完了するたび,スペースキーを押さなくても次に進む
  • 一つの処理が進行中,スペースキーを押してもカウントは止まらない


forced.fluent: スペースキーを押さなくても処理が続く.キャンセル不可.

serial

serialは,複数の処理を一つの連続した処理として実行したいときに指定します.
処理が完了次第,進行命令を待たずに即座に次に進むのはfluentと同様ですが,serialでつなげた処理のキャンセルが連鎖するという点で異なります.


ソースファイル:serial

ソースファイルを実行し,以下のことを確かめてください.

  • 上から順に,一つずつカウントが実行される
  • カウントAをキャンセルすると,カウントBもキャンセルされる
  • カウントCをキャンセルしても,カウントDはキャンセルされない

あるScenarioTaskにserialを指定すると,その一つ後ろのScenarioTaskと直列に接続されます.そして,前者がキャンセルされた瞬間に,後者もキャンセル可能であればキャンセルされます.

例えば,カウントBにはstdを指定していたのでカウントAのキャンセルが連鎖していますが,カウントDにはキャンセル不可のforcedを指定していたので,カウントCがキャンセルされていてもカウントDには影響がありません.


serial: カウントAのキャンセルがカウントBに連鎖


serial: カウントCのキャンセルはカウントDに影響しない

また,serialは多段接続も可能です.2つ以上のScenarioTaskを直列に繋ぐことができ,繋がれているScenarioTaskの内,どれか一つがキャンセルされるとすべてがキャンセルされます.


serialで4つのScenarioTaskを繋げる


カウントAのキャンセルがカウントB,C,Dのキャンセルを引き起こす

paral

paralは,複数の処理を一つの同時処理として実行したいときに指定します.
serial同様,paralでつなげた処理のキャンセルが連鎖します.
ただし,serialは複数の処理が順番に実行されますが,paralでは同時に実行されます.


ソースファイル:paral

ソースファイルを実行し,以下のことを確認してください.

  • カウントAとB,そしてカウントCとDがそれぞれ同時に実行される
  • カウントAをキャンセルすると,カウントBもキャンセルされる
  • カウントCをキャンセルしても,カウントDはキャンセルされない

あるScenarioTaskにparalを指定すると,その一つ後ろのScenarioTaskと並列に接続されます.また,serial同様,paralで繋がったScenarioTaskのキャンセルは連鎖しますが,実際に後続のScenarioTaskがキャンセルされるかどうかは,それに指定されているトークンコードによって異なります.


paral: stdにはキャンセルが連鎖


paral: forcedにはキャンセルが連鎖しない

また,paralも多段接続が可能です.2つ以上のScenarioTaskを同時に実行することができます.


paralで4つのScenarioTaskを同時に実行する


全てのカウントが同時に実行される.カウントAのキャンセルが全体に波及.

その他

これまでに出てきたすべてのトークンコードに該当しないトークンコードを指定した場合,処理は実行開始と同時に終了します.
見かけの挙動としてはparalを指定した時に近いですが,実際にはScenarioTaskが並列接続されているわけでもありませんし,キャンセルが連鎖するわけでもありません.

実行したまま放っておくという表現が適切かもしれません.


ソースファイル:その他


その他: TaskAが指定されたカウントは独立している

実際に上のソースファイルを実行し,カウントAをキャンセルしてみるとその挙動が分かります.
カウントAとカウントBはparalによって並列に接続されているように見えますが,実際はカウントBは無視され,カウントA, C, Dが並列接続されています.そして,カウントBは他のカウントのキャンセルの影響を全く受けていません.

このように,カウントBは他のカウントとは独立しており,実行したまま放置されていることが分かります.

しかし,放置したまま何もできないというわけではありません.後から,"TaskA"が結びつけられたScenarioTaskの完了を待ったり,キャンセルしたりすることもできます.
その方法については,サンプルゲームを作成するときに改めて解説します.

利用方法についてだけ説明しておくと,上で解説した他のトークンコードでは表現しきれないようなシナリオ進行がある場合に,「その他」のトークンコードを指定することでより細かい表現が可能となります.

おわりに

今回の記事では,シナリオ進行の方法を指定するためのトークンコードについて解説しました.
TaskFlowが用意しているトークンコードを組み合わせて使うことによって,場面に合わせた専用の,一回限りしか使わないようなScenarioTaskを作成する必要がほとんどなくなります.
すなわち,「セリフを表示しながらキャラクターを動かす」ScenarioTaskを用意せずとも,「セリフを表示する」,「キャラクターを動かす」のような,2つのScenarioTaskを柔軟に組み合わせることができます.

TaskFlowが標準で提供するトークンコードを解説しましたが,必要に応じて拡張を行うこともできます.実際に,サンプルゲームの作成では新たなトークンコードを追加していきます.

今回の記事はこれで終わりです.
最後までお読みいただき,ありがとうございました.

Discussion