はじめに
ScenarioFlowで会話シーンを再生するために使用されるコマンドには、処理が即座に完了する同期コマンドと、処理の完了に時間がかかる非同期コマンドがあります。同期コマンドと非同期コマンドの違いで重要なのは、非同期コマンドにはトークンコードと呼ばれるパラメータを渡すことができ、それによって動作の振る舞いを変更することができるということです。
ScenarioFlowにおいてトークンコードは、シナリオスクリプトの構造を単純にしつつ、その表現力を高めるために重要な役割を担っています。言い換えれば、ScenarioFlowで自由自在に会話シーンを記述するためには、トークンコードの性質を理解し、有効活用することが必須であるということです。
今回の記事では、トークンコードについて学習します。いくつか種類があるトークンコードについて、それぞれの性質と使い分けについて理解することを目的とします。
ここでは、SFTextで主に使用されるトークンコードを学びます。
SFTextはScenarioFlowで会話シーン記述のために使用されるスクリプト形式ですが、実は、SFTextで使用できるトークンコードと、実際の会話システムで使用できるトークンコードは一致しません。これは、実際に存在するトークンコードのすべてを使い分けずに済むよう、SFTextが独自のトークンコードを用意しているためです。つまり、会話システムで実際に使われるトークンコードのうちいくつかは、SFTextでは使い分ける必要なく、同一のトークンコードとして記述できます。スクリプトに記述されたSFText独自のトークンコードは、Unityへインポートされるときに、実際の会話システムが理解できる適切なトークンコードに置き換えられます。
会話システムで実際に使用されるトークンコードについては、この記事の最後で触れます。まずは、SFTextで使用される実用的なトークンコードについて学びましょう。
サンプルプロジェクトのインストール
以下のhow-to-choose-tokencode.unitypackage
をUnityのプロジェクトにインポートし、今回の学習に必要なプログラムとオブジェクトの準備をしてください。
サンプルプロジェクトに含まれるクラスとコマンドの中で、重要なものを以下に示します。他のクラスとコマンドについては、記事「SFTextの書き方」を参照してください。
クラス | 役割 |
---|---|
ScenarioManager | 会話システムを構成し、会話シーンを再生するクラス |
ButtonNotifier | Nextボタンを押して会話シーンを進め、Cancelボタンでコマンドをキャンセルする機能を実装する |
SkipActivator | Skipボタンによるスキップモードの切り替えを有効にする |
ShapeAnimator | 画面上の図形を動かすコマンドを定義する |
ScenarioTaskConsumer | 「一般のトークンコード」を扱うためのコマンドを定義する |
コマンド | 実行される演出 | 実装クラス |
---|---|---|
move all shapes to left | 画面上の全図形を左に配置する(位置のリセット) | ShapeAnimator |
move shape to right async | 画面上の指定した図形を右に動かす | ShapeAnimator |
accept task async | シナリオタスクを待機する | ScenarioTaskConsumer |
cancel task async | シナリオタスクをキャンセルする | ScenarioTaskConsumer |
ファイルをインポートしたら、how-to-choose-tokencode
シーンを開くとサンプルシーンを実行できます。このサンプルプロジェクトでは、HierarchyウィンドウのScenarioManager
オブジェクトのScenario Script
プロパティに設定されているシナリオスクリプトが実行されていることになっています。始めに、Story.sftxt
がScenario Script
プロパティに設定されていることを確認し、サンプルシーンを実行してみてください。
このサンプルプロジェクトではNextボタンとCancelボタンが分かれており、前者で会話シーンを進め、後者で実行中の演出をキャンセルします。また、「スキップモード」のオンオフを切り替えるSkipボタンも配置されています。
画面上には三つの図形、円、三角形、四角形が配置されており、サンプルシーンではそれらの図形に対し円、三角形、四角形の順で繰り返し「アニメーションコマンド」が呼び出されます。そして、コマンド呼び出しのたびに、異なるトークンコードが与えられます。トークンコードによって図形がどのように移動するか、また図形の移動中、移動後にNextボタン、Cancelボタン、Skipボタンがどのような振る舞いをするのかを確認することで、各トークンコードの性質を理解することができるでしょう。
スキップモード
トークンコードの性質を理解し、適切に使い分けができるようになるためには、前提として「スキップモード」について知っている必要があります。このスキップモードについては別の記事で再び学習しますが、ここではトークンコードの性質を理解するため、最低限の説明にとどめます。
スキップモードは、プレイヤーが会話シーンにおいて物語を読み飛ばすことを可能にする機能です。サンプルプロジェクトではSkipActivator
クラスでこの機能が実装されており、スキップボタンを押すことでスキップモードを有効にすることができます。実際にスキップモードを有効にしてみると、セリフがスキップ、つまり高速に次々表示され、ある地点でスキップが止まったように見えるのが確認できます。
スキップモードが有効になると、基本的に非同期コマンドがスキップされるようになります。「コマンドがスキップされる」というのは、あるコマンドが実行されてから即座にキャンセルされ、その後すぐに次のコマンドが実行されることを意味します。この振る舞いに、INextNotifier
インターフェースやICancellationNotifier
インターフェースの実装は関与しません。
重要なのは、このスキップは特定のトークンコードが渡された非同期コマンドには適用されないということです。サンプルプロジェクトではスキップモードが有効になったとき、スキップされるコマンドとスキップされないコマンドがありますが、この違いはそれらのコマンドに指定されているトークンコードの違いに由来します。上の実行例では、スキップモードが有効になっているにもかかわらず画面上の円がゆっくりと移動していますが、これはそのアニメーションを引き起こしているコマンドに「スキップ禁止」のトークンコードが指定されていることを意味します。
トークンコードの分類
SFTextで主に使用するトークンコードは、以下の8種類です。
- standard
- forced
- promised
- f-standard
- f-forced
- f-promised
- serial
- parallel
ここでは、これらのトークンコードを「キャンセル許可レベル」、「後続コマンドの開始タイミング」、「直列接続・並列接続」の三つの観点から分類していきます。
キャンセル許可レベル
トークンコードは非同期コマンドに対するパラメータとして渡されますが、非同期コマンドは、その処理の実行途中でキャンセルすることができます。しかし、実際の会話シーンでは、キャンセルしてほしくない、あるいはキャンセルしてはいけない非同期コマンドもあります。例えば、あるセリフが演出上重要であるためプレイヤーに読み飛ばしてほしくないときや、選択肢の表示で、何かしらの選択肢が選択されないとシナリオの分岐先を決定できない場合などです。
トークンコードのstandard, forced, promisedを使い分けることで、その非同期コマンドがどのレベルのキャンセルを許可するのかを指定できます。以下の表に、この指定に使用されるトークンコードとその性質、選択のガイドラインを示します。
トークンコード | キャンセル命令 | スキップ命令 | 選択のガイドライン |
---|---|---|---|
standard, f-standard | 許可 | 許可 | キャンセルされてもいいコマンドに |
forced, f-forced | 禁止 | 許可 | キャンセルされるべきでないコマンドに |
promised, f-promised | 禁止 | 禁止 | キャンセルされてはいけないコマンドに |
キャンセル命令は、CancellationNotifier(ICancellationNotifier
の実装)によって発行される命令です。 ICancellationNotifier
を実装するクラスで宣言されているNotifyCancellationAsync
メソッドが完了したとき、「キャンセル命令」が発行され、その結果非同期コマンドがキャンセルされます。
一方、スキップ命令は、スキップモードにより発行される命令です。スキップモードが有効な間、非同期コマンドが実行されるたびに「スキップ命令」が発行され、非同期コマンドがスキップ、つまりキャンセルされます。
上の表に示される通り、(f-)standardはキャンセル命令もスキップ命令も受け付け、(f-)forcedはキャンセル命令は受け付けないがスキップ命令は受け付け、(f-)promisedはキャンセル命令もスキップ命令も受け付けません。以下にstandard, forced, promisedの動作例を示します。
$standard | move shape to right async |
| Move {Circle} to right |
$standard | move shape to right async |
| Move {Triangle} to right |
$standard | move shape to right async |
| Move {Square} to right |
SFText$forced | move shape to right async |
| Move {Circle} to right |
$forced | move shape to right async |
| Move {Triangle} to right |
$forced | move shape to right async |
| Move {Square} to right |
SFText$promised | move shape to right async |
| Move {Circle} to right |
$promised | move shape to right async |
| Move {Triangle} to right |
$promised | move shape to right async |
| Move {Square} to right |
SFText状況に応じた適切な選択のポイントは、まず「システム上キャンセルはOKか」、次に「演出上そのコマンドがどのくらい重要なのか」です。
例えば、プレイヤーに選択肢を与えるコマンドは、いずれかの選択肢を選択してもらわないとシナリオの分岐先がわからないので、システム上キャンセルはしていけません。そのため、そのようなコマンドにはキャンセル命令の発行もスキップ命令の発行も禁止する(f-)promisedを指定することで、その処理が絶対にキャンセルされないようにすることができます。
一方で、システム上はキャンセルが許される場合、そのコマンドの演出上の重要度を考えます。例えばセリフを表示するコマンドについて、基本的には完全にキャンセルを許可する(f-)standardを指定します。しかし、ストーリー上重要なのでそれを読み飛ばしてほしくはないが、どうしてもプレイヤーがそうしたい場合にはそれを許可したいのであれば(f-)forcedを、絶対に全文を丁寧に読んでほしいのであれば(f-)promisedを指定します。
(f-)forcedと(f-)promisedの違いは特に重要で、前者は「キャンセルされるべきでない」、後者は「キャンセルされてはいけない」を表すと理解しておくと良いでしょう。
後続コマンド開始タイミング
ある非同期コマンドの処理が完了した際、次のコマンドが実行されるのは、通常INextNotifier
インターフェースの実装が定義するNotifyNextAsync
メソッドの処理が完了した後です。例えば、画面がタップされたときにNotifyNextAsync
メソッドが完了するようにしておけば、あるセリフが非同期コマンドによって表示された後、プレイヤーが画面をタップしたときに次の非同期コマンドが実行され、次のセリフが表示され始めるといった挙動が実現できます。
これを言い換えれば、ある非同期コマンドの処理が完了した後、基本的に次のコマンドの開始はNextNotifier(INextNotifier
の実装)によって「進行命令」が発行されるまで遅延されるということです。しかし、特定のトークンコードを非同期コマンドに渡すことによって、その処理が完了した後、進行命令を無視して即座に次のコマンドを開始させることができます。以下に、この観点でのトークンコードの分類とその性質、選択のガイドラインを示します。
トークンコード | 進行命令 | 選択のガイドライン |
---|---|---|
standard, forced, promised | 待機 | セリフの表示に |
f-standard, f-forced, f-promised | 無視 | アニメーションや選択肢の表示に |
standard, forced, promisedの頭にf-
を付けると、その非同期コマンドが完了した後、進行命令を無視して即座に次のコマンドが開始されます。逆にf-
がない場合、非同期コマンドの完了後、通常通り進行命令を待機して、それが発行されてから次のコマンドが開始されます。ちなみに、f-
は”fluent”の頭文字です。
以下に、standardとf-standardの動作例を示します。コマンド完了後、Nextボタンを押さずとも次のコマンドが開始されていることに注目してください。
$standard | move shape to right async |
| Move {Circle} to right |
$standard | move shape to right async |
| Move {Triangle} to right |
$standard | move shape to right async |
| Move {Square} to right |
SFText$f-standard | move shape to right async |
| Move {Circle} to right |
$f-standard | move shape to right async |
| Move {Triangle} to right |
$f-standard | move shape to right async |
| Move {Square} to right |
SFTextfluentでないトークンコード (standard, forced, promised)は、主にセリフの表示をするコマンドに対して指定します。あるセリフが表示された後、画面のタップなどのプレイヤーのアクションを待ってから次のセリフを表示するという、自然な振る舞いを実現することができます。standard, forced, promisedのどれを使うかは、前述の通り、そのセリフの重要度によります。
fluentなトークンコード(f-standard, f-forced, f-promised)は、アニメーションや、選択肢の表示を行うコマンドに使用します。そのようなコマンドにfluentでないトークンコードを指定すると、あるアニメーションが終わった後、もしくは選択肢をプレイヤーが選択した後に、プレイヤーが何らかのアクションを起こさないと次のセリフに移行しないという少々不自然な振る舞いとなってしまいます。fluentなトークンコードのうちどれを使うかは、コマンドによって引き起こされる演出の重要度と、システム上キャンセルが許されるかどうかによって決めます。
直列接続・並列接続
トークンコードserialもしくはparallelを指定することで、その非同期コマンドを次の非同期コマンドと結合させ、一つのコマンドとして実行することができます。次の表に、これらのトークンコードの性質をまとめます。
トークンコード | 接続方式 | 進行命令 | キャンセル命令 | スキップ命令 |
---|---|---|---|---|
serial | 直列 | 無視 | 後続と同じ | 後続と同じ |
parallel | 並列 | 後続と同じ | 後続と同じ | 後続と同じ |
serialは直列接続を意味し、接続されたコマンドが順番に実行されます。fluentなトークンコードを指定したときと同様、一つのコマンドが完了した後は即座に次のコマンドが開始されますが、fluentなコマンドを書き連ねた場合と異なり、接続されたコマンドのうちどれか一つがキャンセルされると、他のコマンドもすべてキャンセルされます。
$serial | move shape to right async |
| Move {Circle} to right |
$serial | move shape to right async |
| Move {Triangle} to right |
$standard | move shape to right async |
| Move {Square} to right |
SFTextparallelは並列接続を意味し、接続されたコマンドは同時に実行されます。コマンドが同時に実行されるので、コマンドがキャンセルされるときはすべてのコマンドが同時にキャンセルされます。
$parallel | move shape to right async |
| Move {Circle} to right |
$parallel | move shape to right async |
| Move {Triangle} to right |
$standard | move shape to right async |
| Move {Square} to right |
SFText注意として、serialもしくはparallelを指定した非同期コマンドのキャンセル許可レベルは、それが接続されているコマンドのものと同じになります。例えばserialもしくはparallelを指定した非同期コマンドが(f-)standardを指定したコマンドに接続されていればキャンセル命令もスキップ命令も受け入れられますが、(f-)promisedを指定したコマンドに接続されていればそのどちらによってもキャンセルはされません。
これらのトークンコード、serialとparallelを使う場面としてよくあるのは、キャラクターに対する小さなアニメーションをつなげて一つのアニメーションにするようなときです。キャラクターに複雑な動きをさせたいときに、それを可能にする専用のコマンドを用意しているとコマンド数がどんどん増えていきますが、単純なアニメーションを引き起こす小さなコマンドをいくつか用意してそれらを組み合わせるようにすれば、ある程度のコマンド数で複雑なアニメーションを作ることができます。
トークンコードまとめ
トークンコードの性質を以下の表にまとめます。
トークンコード | 進行命令 | キャンセル命令 | スキップ命令 |
---|---|---|---|
standard | 待機 | 許可 | 許可 |
forced | 待機 | 禁止 | 許可 |
promised | 待機 | 禁止 | 禁止 |
f-standard | 無視 | 許可 | 許可 |
f-forced | 無視 | 禁止 | 許可 |
f-promised | 無視 | 禁止 | 禁止 |
serial | 無視 | 後続と同じ | 後続と同じ |
parallel | 後続と同じ | 後続と同じ | 後続と同じ |
進行命令はINextNotifier
インターフェースの実装から発行される、あるコマンドが完了した後に次のコマンドを開始するために命令で、キャンセル命令はICancellationNotifier
の実装から発行される、あるコマンドの実行中に、そのコマンドのキャンセルを引き起こす命令です。スキップ命令は、スキップモードが有効になっている間に発行され続ける、コマンドの開始とキャンセルを繰り返す命令です。
まず、基本のトークンコードとしてstandard、forced、promisedがあり、これらが指定されたコマンドが完了した後は、進行命令が発行されるのを待機してから次のコマンドが実行されます。そして、キャンセル命令とスキップ命令が許可されるかどうかは指定されているトークンコードによります。
次に、上記のトークンコードの頭にf-
を付けたfluentなトークンコード、f-standard、f-forced、f-promisedがあり、これらが指定されたコマンドが完了した後は、進行命令を待たず、即座に次のコマンドが開始されます。そして、キャンセル命令とスキップ命令が許可されるかどうかはトークンコードによります。
最後に、複数のコマンドを接続するためのトークンコード、serialとparallelがあり、これらのトークンコードを指定したコマンドは後続の非同期コマンドと接続されます。serialの場合、接続されたコマンドは順番に実行され、fluentなトークンコードを指定したときのようにあるコマンドの完了後は次のコマンドが即座に開始されますが、一つのコマンドがキャンセルされると他のコマンドも一斉にキャンセルされるという点でfluentなトークンコードと異なります。parallelの場合、接続されたコマンドは同時に実行され、キャンセルされるときはすべてが同時にキャンセルされます。これらのトークンコードが指定されたコマンドのキャンセル許可レベルは、接続先のコマンドのそれと一致します。
トークンコードの選び方としては、対象とするコマンドの役割に注目します。例えば、セリフを表示するコマンドの場合、セリフが表示された後、画面のクリックなどのプレイヤーアクションを待ってから次のセリフを表示させたいのでfluentでないトークンコードを指定します。キャンセル許可レベルは、そのセリフの重要度によって決めます。一方で選択肢を表示させるコマンドの場合、システム上キャンセルはできないのでpromisedなトークンコードを指定します。fluentなものでもfluentでないものでも動作上は問題ありませんが、選択肢を選択後は即座に次の演出に移行した方が自然な演出になるのでfluentな方、f-promisedを選択します。
その他の機能
ここまでに、特にSFTextにおける、トークンコードの主な用法について学習しました。このセクションでは、トークンコードについてその他の用法を学習します。ここで取り扱う機能はほとんど使用されないかつ、使用も推奨されないため、読み飛ばしても構いません。
12種類のトークンコード
すでに学習した8種類のトークンコードは、正確にはSFTextにおいて使用できるトークンコードです。実際にScenarioFlowのシステム内で取り扱われるトークンコードは、以下の12種類があります。
ベース | 標準 | Fluent | Serial | Parallel |
---|---|---|---|---|
standard | standard | f-standard | s-standard | p-standard |
forced | forced | f-forced | s-forced | p-forced |
promised | promised | f-promised | s-promised | p-promised |
接頭辞のない標準形式と、接頭辞f-
のついたFluent形式は、SFTextにおいて使われるそれらと同じです。接頭辞s-
とp-
がそれぞれ付いたSerial形式とParallelについては、SFTextで使用されるserial
トークンコードとparallel
トークンコードとは異なり、接続先のコマンドに関係なくキャンセル許可レベルを指定できるという点のみ異なります。
重要な点は、Serial形式とParallel形式の使用において、それらを指定するコマンドのキャンセル許可レベルは、それぞれが接続されるコマンドのキャンセル許可レベルと同一のものにする場合がほとんどで、またそれが最も自然な演出につながるということです。そのために、SFTextでは単純化のためにトークンコードserial
とparallel
でSerial形式とParallel形式のトークンコードが代替され、UnityへのSFTextのインポート時にserial
とparallel
が、それらが指定されたコマンドの接続先と同じキャンセル許可レベルを持つSerial形式またはParallel形式のトークンコードで置き換えられるようになっています。実際に、SFTextのInspectorウィンドウを見ると、そのような変換が行われていることがわかります。
特別な理由がない限りは、コマンドを直列または並列接続する場合、s-standardやp-forcedなどで明示的にキャンセル許可レベルを指定することはせず、単にserial
やparallel
と書くのが良いでしょう。
一般のトークンコード
SFTextでは主に8種類のトークンコードを使用し、システム内では12種類のトークンコードが取り扱われていることを学習しました。実は、それ以外のトークンコードも渡すことができます。12種類のトークンコードを「特別なトークンコード」、それ以外のトークンコードを「一般のトークンコード」と呼びます。
一般のトークンコードを指定した非同期コマンドはScenarioTaskExecutor
クラスの中に「シナリオタスク」として格納され、他の特別なトークンコードが指定された非同期コマンドと隔離されて実行されます。その非同期コマンドが開始した後は、parallel同様に即座に次のコマンドが開始されますが、後続の非同期コマンドとリンクされているわけではありません。
シナリオタスクは、ScenarioTaskExecutor
クラスが実装するIScenarioTaskStorage
のメンバメソッドであるAcceptAsync
メソッドで待機することができ、Cancel
メソッドでキャンセルすることができます。これらのメソッドを利用することで、一般のトークンコードを取り扱うためのコマンドを作成できます。例として、サンプルプロジェクトのScenarioTaskConsumer
クラスを参照してください。
using Cysharp.Threading.Tasks;
using ScenarioFlow;
using ScenarioFlow.Scripts.SFText;
using ScenarioFlow.Tasks;
using System;
using System.Threading;
namespace HowToChooseTokenCode
{
public class ScenarioTaskConsumer : IReflectable
{
private readonly IScenarioTaskStorage scenarioTaskStorage;
public ScenarioTaskConsumer(IScenarioTaskStorage scenarioTaskStorage)
{
this.scenarioTaskStorage = scenarioTaskStorage ?? throw new ArgumentException(nameof(scenarioTaskStorage));
}
[CommandMethod("accept task async")]
[Category("Task")]
[Description("Await scenario tasks with the specified token code.")]
[Snippet("Accept {${1:token code}}")]
public async UniTask AcceptScenarioTasksAsync(string tokenCode, CancellationToken cancellationToken)
{
await scenarioTaskStorage.AcceptAsync(tokenCode, cancellationToken);
}
[CommandMethod("cancel task")]
[Category("Task")]
[Description("Cancel scenario tasks with the specified token code.")]
[Snippet("Cancel {${1:token code}}")]
public void CancelTask(string tokenCode)
{
scenarioTaskStorage.Cancel(tokenCode);
}
}
}
C#accept task async
コマンドは、指定された一般のトークンコードと結び付けられているシナリオタスクの完了を待機し、cancel task
コマンドはキャンセルを引き起こします。これらのコマンドを使った、一般のトークンコードの動作例を確認しましょう。ここから紹介する動作例は、サンプルプロジェクト内のGeneralTokenCode.sftxt
で確認できます。このSFTextを、ScenarioManagerオブジェクのScenario Script
プロパティにセットしてください。
以下のスクリプトでは、すべての図形が同時に移動し、一見最初の二つにparallelを指定したときと同じように見えます。しかし、parallelが指定された二番目のコマンドとf-standardが指定された三番目のコマンドはリンクされているのに対し、”task1″トークンコードが指定された、一番最初のコマンドは他のどのコマンドともリンクしていません。このことは、アニメーションをキャンセルしてみると良くわかります。アニメーションが開始してからCancelボタンを押すと、parallelとf-standardが指定されたコマンドはキャンセルされますが、”task1″が指定されたコマンドはキャンセルされません。コマンドのキャンセル後はaccept task async
でtask1のコマンドの完了が待機され、accept task async
にはpromisedが指定されているのでキャンセルできません。
$task1 | move shape to right async |
| Move {Circle} to right |
$parallel | move shape to right async |
| Move {Triangle} to right |
$f-standard | move shape to right async |
| Move {Square} to right |
| |
$promised | accept task async |
| Accept {task1} |
SFText同じトークンコードを指定したシナリオタスクは、一括で完了を待機またはキャンセルします。以下のスクリプトでは、cancel task
コマンドによって、”task2″を指定したシナリオタスクをすべてキャンセルしています。アニメーションが完了する前にセリフを読み飛ばすことで、キャンセルの挙動が確認できます。
$task2 | move shape to right async |
| Move {Circle} to right |
$task2 | move shape to right async |
| Move {Triangle} to right |
$task2 | move shape to right async |
| Move {Square} to right |
Sheena | Click the cancel button and next button quickly to cancel the animation! |
$sync | cancel task |
| Cancel {task2} |
SFText一般のトークンコードにより、コマンド実行の自由度は上がりますが、以下の2つの理由からこの機能の使用は推奨されません。
- 処理の流れが複雑になり、シナリオスクリプトから実際に再生される会話シーンを想像するのが難しくなること
- 会話シーンにセーブ機能を実装する場合、この機能の乱用は容易に不具合を引き起こすこと
実用上は、一般のトークンコードを使用することなく、特別なトークンコードのみで十分柔軟に会話シーンを構成できます。
おわりに
今回の記事では、非同期コマンドにパラメータとして渡されるトークンコードについて学習しました。トークンコードは、1つの非同期コマンドを様々な方法で実行することを可能にする、柔軟に会話シーンを記述する上での強力な武器です。他方で、不適切なトークンコードの指定は不自然な演出を招いたり、システム上の不具合を招いたりします。そのため、各トークンコードの性質を理解し、状況に応じて適切なトークンコードを選択できるようにしましょう。
コメント