【ScenarioFlow】その他の機能:シナリオスクリプトの統合、トークンコードの判定

ScenarioFlow
ツールのバージョン
  • Unity: 2022.3.35f1
  • UniTask: 2.5.4
  • ScenarioFlow: 1.1.0
  • SFText Extension Pack: 1.1.0
    • Syntax: v1.1.0
    • Formatter: v1.0.0
    • Utility: v1.0.0

はじめに

今回は、ScenarioFlowにおいて複数のシナリオスクリプトを統合して1つのシナリオスクリプトとして使用するための機能と、非同期コマンドに指定されたトークンコードを取得するための機能について学習します。

これらの機能はScenarioFlowで会話シーンを作成するためには必須ではありませんが、状況によっては便利に使用することができます。特に、トークンコードの判定は、会話シーンのセーブ機能を実装する際に必要になります。

サンプルプロジェクトの準備

以下のother-features.unitypackage をUnityのプロジェクトにインポートし、今回の学習に必要なプログラムとオブジェクトを準備してください。

サンプルプロジェクトには以下の5つのシナリオスクリプトが含まれていますが、実際に実行するのはStory (Composite Scenario Script) です。これがシーン上のScenarioManagerScenarioScript プロパティに設定されていることを確認の上、サンプルシーンを実行してみてください。

  • Story (Composite Scenario Script)
  • Story-Intro (SFText)
  • Story-Composite (SFText)
  • Story-TokenCode (SFText)
  • Story-End (SFText)
サンプルシーンの実行

今回のサンプルでは、(1)複数のシナリオスクリプトから構成された会話シーンが実行されていること、(2)各非同期コマンドに指定されたトークンコードがログに出力されていること、がポイントになります。サンプルプロジェクト内のコマンドの詳細については、【ScenarioFlow】SFTextの書き方【ScenarioFlow】実践的な機能:シナリオ分岐と早送りなどを参照してください。

Compositeスクリプトによるシナリオスクリプトの統合

Compositeスクリプトの使い方

Compositeスクリプトを使用することで、複数のシナリオスクリプトを統合し、1つのシナリオスクリプトとして使用することができます。

Compositeスクリプトは、Projectウィンドウで右クリックから、Create/ScenarioFlow/Composite Scenario Script を選択することで作成できます。

新しいCompositeスクリプトの作成

Compositeスクリプトを作成したら、統合したいシナリオスクリプトをScenario Scripts プロパティに登録します。

サンプルプロジェクトには、Story という名前のCompositeスクリプトが作成されており、このCompositeスクリプトは4つのシナリオスクリプトが登録されています。このとき、Story Compositeスクリプトを実行することで、登録された4つのシナリオスクリプトの会話シーンが統合された、1つの会話シーンが再生されます。

Story Compositeスクリプト。Story-IntroStory-CompositeStory-TokenCodeStory-End の4ファイルから構成される。

Compositeスクリプトを使用するときのポイント

Compositeスクリプトの使用に関して、以下の点を覚えておきましょう。

  • Compositeスクリプトに登録された順番で、シナリオスクリプトは統合される
  • 登録されたシナリオスクリプトは互いに独立している
  • 登録されたシナリオスクリプトをまたいでラベルを参照できる

まず、Compositeスクリプトは登録された複数のシナリオスクリプトから1つのシナリオスクリプトを生成しますが、登録された順番により、生成される内容は変わります。Compositeスクリプトは、登録されたシナリオスクリプトを上から順に統合します。Story Compositeスクリプトでは、始めはStory-IntroStory-CompositeStory-TokenCodeStory-End の順でシナリオスクリプトが統合されていますが、シナリオスクリプトの登録順を変更すると、Inspectorウィンドウに表示されるデータの内容も変化することを確認してください。

次に、Compositeスクリプトに登録されたシナリオスクリプトは互いに独立しています。これは例えば、あるCompositeスクリプトに登録されたSFTextのマクロスコープは、他の登録されたSFTextに影響しないということです。

最後に、ラベルについて、Compositeスクリプトに登録されるシナリオスクリプトは、他の登録されたシナリオスクリプト内にあるラベルを参照できます。Story-Intro SFText内のshow two selections async コマンドが、それぞれStory-Composite SFTextとStory-TokenCode SFTextで宣言されている、composite script lecture ラベルとtoken code lecture ラベルを参照していることを確認してください。しかし、逆に、登録されたシナリオスクリプト間で同名のラベルが存在できないことにも注意してください。

Story-Intro.sftxtの一部(他のSFTextのラベルを参照)
Sheena      | So could I ask you which one you want to know?                            | 
            |                                                                           | 
$f-promised | show two selections async                                                 | 
            | {How to merge multiple scenario scripts}                                  | 
            | --- Jump to {composite script lecture}                                    | 
            | {How to retrieve specified token codes}                                   | 
            | --- Jump to {token code lecture}                                          | 
SFText
Story-Composite.sftxtの一部
#label    | //============ {composite script lecture} ============//   | 
$parallel | change character image async                               | 
          | Change the icon to {smile}                                 | 
Sheena    | OK! Let's learn how to merge multiple scripts.             | 
SFText
Story-TokenCode.sftxtの一部
#label    | //============ {token code lecture} ============//                          | 
$parallel | change character image async                                                | 
          | Change the icon to {smile}                                                  | 
Sheena    | OK! Let's learn how to retrieve the token code specified for a command.     | 
SFText

Compositeスクリプトの使いどころ

Compositeスクリプトを使用する場面としては、長い会話シーンを複数人で作成する場合が考えられます。複数人で小さな会話シーンをそれぞれ作成し、それらをCompositeスクリプトで結合すれば、それぞれが独立して作業したうえで、1つの会話シーンを作成することができます。

コマンドに指定されたトークンコードの判定

ITokenCodeGetterインターフェースを介したトークンコードの取得

ScenarioTaskExecutor クラスはTokenCode プロパティを実装しており、このプロパティをITokenCodeGetter インターフェースを介して読み取ることで、あるコマンドの呼び出し時、そのコマンドに指定されたトークンコードを取得できます。(インターフェースを介してプロパティにアクセスするのは、クラスが実装する関係のない他のメンバーにアクセスしないようにするためにです。)

TokenCode プロパティはコマンドが呼び出されるたびに値が変わるので、適切に値を読み取るためには、値を読み取る場所とタイミングが重要になります。

TokenCode プロパティを読み取るのに適した方法の一つは、IScenarioTaskExecutorインターフェースデコレーターを作成し、そのメンバメソッドであるExecuteAsync メソッドの処理の最初に、トークンコードの値を読み取ることです。

IScenarioTaskExecutor インターフェースは、ScenarioTaskExecutor クラスが実装するインターフェースで、ExecuteAsync(UniTask, CancellationToken) をメンバメソッドに持ちます。ScenarioBookReader クラスのコンストラクタには、基本的にはScenarioTaskExecutor クラスのインスタンスが渡されますが、実際にScenarioBookReader クラスのコンストラクタが要求するのはIScenarioTaskExecutor インターフェースの実装なので、作成したデコレーターをコンストラクタに渡すことで、作成したデコレーターの機能をシステムに組み込むことができます。

サンプルプロジェクトの実装例

サンプルプロジェクトの例を見てみます。サンプルプロジェクトでは、ScenarioTaskExecutorTokenLoggingDecorator クラスが、IScenarioTaskExecutor インターフェースのデコレーターとして実装されています。

ScenarioTaskExecutorTokenLoggingDecorator.cs
using Cysharp.Threading.Tasks;
using ScenarioFlow.Tasks;
using System;
using System.Threading;
using UnityEngine;

namespace OtherFeatures
{
	public class ScenarioTaskExecutorTokenLoggingDecorator : IScenarioTaskExecutor
    {
        private readonly IScenarioTaskExecutor scenarioTaskExecutor;
        private readonly ITokenCodeGetter tokenCodeGetter;

        public ScenarioTaskExecutorTokenLoggingDecorator(IScenarioTaskExecutor scenarioTaskExecutor, ITokenCodeGetter tokenCodeGetter)
        {
            this.scenarioTaskExecutor = scenarioTaskExecutor ?? throw new ArgumentNullException(nameof(scenarioTaskExecutor));
            this.tokenCodeGetter = tokenCodeGetter ?? throw new ArgumentNullException(nameof(tokenCodeGetter));
        }

        public UniTask ExecuteAsync(UniTask scenarioTask, CancellationToken cancellationToken)
        {
            // トークンコードを取得
            var tokenCode = tokenCodeGetter.TokenCode;
            Debug.Log(tokenCode);
            return scenarioTaskExecutor.ExecuteAsync(scenarioTask, cancellationToken);
        }
    }
}
C#

このデコレーターにおけるExecuteAsync メソッドは、その最初の処理でITokenCodeGetter インターフェースの実装からトークンコードを取得し、それをログに出力したうえで、内部に持つ別のIScenarioTaskExecutor の実装のExecuteAsync を実行してその処理を終えています。ExecuteAsync メソッドのパラメータ、scenarioTask は、呼び出された非同期コマンドそのものを表します。非同期コマンドに関する処理はScenarioTaskExecutor クラスが行うので、デコレーターのメソッド中ではこのパラメータに対する操作はしてはいけません。

scenarioTaskExecutortokenCodeGetter には、どちらにもScenarioManager クラスにて、ScenarioTaskExecutor クラスのインスタンスが割り当てられます。また、ScenarioManager クラスでは、ScenarioBookReader クラスのコンストラクタに対し、ScenarioTaskExecutor のインスタンスではなく、デコレーターであるSceanarioTaskExecutorTokenLoggingDecorator クラスのインスタンスが渡されていることにも注意してください。

ScenarioManager.csの一部
ScenarioTaskExecutor scenarioTaskExecutor = new(buttonNotifier, buttonNotifier);
ScenarioTaskExecutorTokenLoggingDecorator scenarioTaskExecutorTokenLoggingDecorator = new(scenarioTaskExecutor, scenarioTaskExecutor);
ScenarioBookReader scenarioBookReader = new(scenarioTaskExecutorTokenLoggingDecorator);
C#

これらの実装により、会話シーン実行時、呼び出された非同期コマンドに対して渡されたトークンコードを取得することができます。サンプルシーンの実行時、シナリオスクリプトで指定された、各コマンドに対応するトークンコードがログに出力されることを確認してください。

サンプルシーンの実行時、呼び出されたコマンドに指定されたトークンコードがログに出力されている

指定されたトークンコードを取得する場面

コマンドに指定されたトークンコードを取得しなければならないようなケースは、例えば会話システムにセーブ機能を実装する場合です。ここでのセーブ機能とは、会話シーンの再生中、セリフ一つ一つの単位で、任意のタイミングでセーブができる機能のことです。

SceanarioFlowにおいてこのようなセーブ機能を実装するためには、適切なタイミングで定期的にシーン上の情報(キャラの位置、キャラ画像、背景、音楽など)をすべてキャプチャする必要があります。適切なタイミングとは、実行中のコマンドが1つもなく、シーン上の環境が変化しない瞬間です。そして、実行中のコマンドが1つもない瞬間とは、例えばプレイヤーのアクション(画面のタップなど)を待っているときです。プレイヤーのアクションを待機している瞬間とは、例えばfluentでないトークンコード(standard, forced, promised)が指定されたコマンドが完了した直後です。

以上を踏まえて、セーブのために必要な情報をキャプチャするのに適切なタイミングを判断するために、コマンドに指定されたトークンコードを取得する必要がある場合がある、と言えます。

もちろん、セーブ機能の実装方法によっては、トークンコードの取得が必要ない場合もあります。しかし、そのような機能の実装方法に幅を持たせるために、ITokenCodeGetter インターフェースが用意されています。

まとめ

今回は、複数のシナリオスクリプトを1つのシナリオスクリプトへ統合する方法と、あるコマンドに指定されたトークンコードを取得する方法について学習しました。

まず、Compsoiteスクリプトを使用することにより、登録したシナリオスクリプトを統合し、1つのシナリオスクリプトとして扱うことができます。ポイントは以下の通りです。

  • Compositeスクリプトに登録された順番で、シナリオスクリプトは統合される
  • 登録されたシナリオスクリプトは互いに独立している
  • 登録されたシナリオスクリプトをまたいでラベルを参照できる

次に、ITokenCodeGetter インターフェースのTokenCode プロパティにより、呼び出された非同期コマンドに指定されたトークンコードを取得することができます。この方法によるトークンコードの取得では、プロパティにアクセスする場所とタイミングに注意する必要があり、適した方法の一つは、IScenarioTaskExecutor インターフェースのデコレーターを作成し、ExecuteAsync メンバメソッドの最初の処理でプロパティの値を読み取ることです。

今回学習した機能が活用できる場面は限られていますが、特定の状況では有効に活用することができます。使う機会は少ないかもしれませんが、このような機能があるということを頭の片隅に置いておくと良いでしょう。

コメント