【ScenarioFlow】ライブラリ紹介:ScenarioFlowで会話シーンをつくろう

ScenarioFlow
使用しているツールのバージョン
  • Unity: 2022.3.35f1
  • ScenarioFlow: 1.1.0

ScenarioFlowとは

ScenarioFlowは、ゲームエンジンのUnityで使用できるライブラリです。このライブラリを使用して、ゲームの会話シーンを効率的に作成することができます。現在、Unity公式アセットストアでダウンロードができます。

ScenarioFlow | Integration | Unity Asset Store
Use the ScenarioFlow from .PROLOGUE on your next project. Find this integration tool & more on the Unity Asset Store.

ScenarioFlowの特徴は、システムの拡張性が高くどのようなプロジェクトのどのような会話シーンにも対応することを目指して設計されていることです。ScenarioFlowは、会話シーンの作成において本質的に必要な機能のみを提供し、またそのシステムはDependency Injectionパターンに基づく設計によって、非常に拡張性の高いものになっています。ScenarioFlowの最大の価値は、会話シーンを作成するための、スケーラビリティの高い新たなアーキテクチャを提供していることにあります。

拡張性の高いシステムをプログラマー向けに提供する一方で、ScenarioFlowは、ライター向けの新たなシナリオ記述用スクリプトも提供します。そのスクリプトはSFTextと呼ばれ、単純な文法と本物の台本のような外見を持ち、読みやすく書きやすいものとなっています。ScenarioFlowでは、ライターが書いたシナリオ記述用スクリプトをプログラマーが書いたプログラムで実行することで、会話シーンが動きます。

まとめると、ScenarioFlowはプログラマー向けに拡張性の高いシステムを、ライター向けに書きやすく読みやすいスクリプト形式を提供します。ScenarioFlowにより、プログラマーとライター、それぞれの作業効率が大いに向上するでしょう。

ScenarioFlowを使うべきケース・使わないべきケース

ScenarioFlowの目的は、「開発者が会話シーンを効率的に作成できるようにすること」です。しかし、より正確には、「開発者が会話シーンを作るためのシステムを効率的に作成できるようにすること」がSceanrioFlowの目的になります。

会話シーンを作成するためのライブラリは、ScenarioFlow以外にもすでにいくつかあります(Naninovelなど)が、ScenarioFlowと既存のライブラリの最大の違いは、開発難易度と開発自由度のトレードオフにあります。

例えば、会話シーン作成ライブラリでは、スクリプトから「コマンド」を呼び出すことで会話シーンが再生されますが、既存のライブラリが多くのコマンドをデフォルトで用意しているのに対し、ScenarioFlowは一つもコマンドを提供しません。また、既存のライブラリはシナリオ進行用の機能(クリックしたら次のセリフに進む機能や、会話ログ機能など)をデフォルトで提供しますが、ScenarioFlowはそのような機能も提供しません。ScenarioFlowが提供するのは、コマンドや会話ログなど、会話シーン作成に必要な機能をプロジェクトにうまく組み込むための枠組みです。ScenarioFlowは、新機能を簡単に追加することができる枠組みを提供するが、あくまで具体的な、プロジェクトで必要とされる機能は開発者自身に開発してもらうという立場をとっています。

ScenarioFlowと既存ライブラリの関係性をよく理解するために、一つの例を挙げます。あなたはお腹が空いていて食事をとりたいとします。既存のライブラリを使うのはレストランに行くようなもので、ScenarioFlowを使うのはデパートに行くようなものです。レストランに行けば、自分は何もすることはなく美味しい料理を食べることができますが、食べられるものはメニューに掲載されている料理に限られ、価格もある程度するでしょう。デパートに行けば、食材や調理器具などを購入することができ、手間はかかりますが比較的自由に食べる料理を選択でき、予算も選択できます。

つまり、既存のライブラリでは「豊富な機能をデフォルトで持っているため簡単に会話シーンを作れるが、用意されていない機能を追加するのは大変だったり、余計な機能がついていたりする」のに対し、ScenarioFlowでは「デフォルトで用意されている機能は少ないが、独自の機能を追加するのは簡単で、必要な機能だけ追加すればよい」ということになります。

会話シーン上の演出を呼び出すためのコマンドや、セリフを次に進めるためのトリガーなど、会話シーンを再生するうえで必要な機能を総じてここでは会話システムと呼んでいます。ScenarioFlowでできるのは、自身のプロジェクトに合った会話システムを作成することです。それにより、結果的に会話シーンを効率的に作成することができます。

結論として、独自性の高い会話シーンを作成したい、もしくは最小限のシステムで会話シーンを運用したい場合は、ScenarioFlowの使用をお勧めします。ただし、必要な機能は自身で用意しなければならないので、ある程度のプログラムのコストはかかります。

必要な前提知識

ScenarioFlowでは、会話システムを構築するため、ある程度のコードを自身で書く必要があります。これは、ある程度のプログラムに関する知識が必要であることも意味します。

具体的には、ScenarioFlowを使いこなすためには以下の知識が必要です。

  • UniTask
  • Dependency Injection (DI)

UniTaskは、Unityにおいて非同期処理を効率的に扱うためのライブラリです。会話シーンの実装には多くの非同期処理を必要とし、ScenarioFlowは非同期処理を扱うためにUniTaskを採用しています。

Dependency Injection (DI) は、拡張性の高いシステムを構築するためのデザインパターンであり、ScenarioFlowはそれに基づいて設計されています。C#におけるインターフェースに関する知識があれば、DIについて知らなくてもScenarioFlowの使い方を理解することはできるのですが、DIに関する知識があれば、ScenarioFlowをより効果的に活用することができます。

UniTaskやDIについて良く知らないという方も、ScenarioFlowの使用を避ける必要はありません。そのためにこのブログが立ち上げられています!

このブログではUniTaskやDIを含めて、ScenarioFlowを活用するために必要な周辺知識も解説していきます。記事は順次追加されるので、それらを参照したり、わからないことがあれば気軽にコメントしたりしてください!

ScenarioFlowの特徴:高い拡張性

すでに述べた通り、ScenarioFlowの特徴は、その高い拡張性です。その拡張性の高さは、主にDIの適用とリフレクションの活用によって得られており、それぞれシステム全体の拡張性の高さ、コマンドの拡張性の高さに寄与しています。

ここでは、それらの戦略によって得られる利点を、具体例とともに見ていきます。

DIに基づく高いシステム拡張性

DIは、簡単に言えばインターフェースをうまく使ってシステムの拡張性を高める手法です。ScenarioFlowにおいて何らかの機能を拡張するためには、基本的には決まったインターフェースを実装する新たなクラスを作成することになります。

例えば、ScenarioFlowはシナリオ進行を管理するためのトリガーを規定するためのインターフェースを提供しており、その一つがINextNotifier インターフェースです。このインターフェースを通して、一つのセリフが表示された後に、次のセリフに進めるためのトリガーを規定することになります(厳密にはその仕様とは異なりますが、現時点ではその理解で問題ありません)。

例えば、INextNotifier インターフェースを実装する次のクラスを定義することによって、スペースキーを押したときにセリフを次に進めるような機能を実装できます。

SpacekeyNextNotifier.cs
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using ScenarioFlow.Tasks;
using System.Threading;
using UnityEngine;

public class SpacekeyNextNotifier : INextNotifier
{
    public UniTask NotifyNextAsync(CancellationToken cancellationToken)
    {
        return UniTaskAsyncEnumerable.EveryUpdate()
            .Select(_ => Input.GetKeyDown(KeyCode.Space))
            .Where(x => x)
            .FirstOrDefaultAsync(cancellationToken: cancellationToken);
    }
}
C#

SpaceKeyNextNotifier クラスは、INextNotifier インターフェースのメンバメソッドであるNotifyNextAsync メソッドを実装しています。ここでは、そのメソッドは「スペースキーが一度押されると完了する」非同期処理になっています。

あるボタンを押したときに次のセリフに進むような機能を実装したい場合は、次のようなクラスを定義します。

SingleButtonNextNotifier.cs
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using ScenarioFlow.Tasks;
using System.Threading;
using UnityEngine.UI;

public class SingleButtonNextNotifier : INextNotifier
{
	private readonly Button button;

	public SingleButtonNextNotifier(Button button)
	{
		this.button = button;
	}

	public UniTask NotifyNextAsync(CancellationToken cancellationToken)
	{
		return button.OnClickAsAsyncEnumerable(cancellationToken: cancellationToken)
			.FirstOrDefaultAsync(cancellationToken: cancellationToken);
	}
}
C#

SingleButtonNextNotifier クラスでは、同様にNotifyNextAsync メソッドが定義されていますが、この場合は「ボタンが押されたときに完了する」非同期処理となっています。

これら二つのトリガーを組み合わせることもできます。

C#
using Cysharp.Threading.Tasks;
using ScenarioFlow.Tasks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

public class CompositeAnyNextNotifier : INextNotifier
{
    private readonly IEnumerable<INextNotifier> nextNotifiers;

    public CompositeAnyNextNotifier(IEnumerable<INextNotifier> nextNotifiers)
    {
        this.nextNotifiers = nextNotifiers;
    }

    public UniTask NotifyNextAsync(CancellationToken cancellationToken)
    {
        return UniTask.WhenAny(nextNotifiers.Select(notifier => notifier.NotifyNextAsync(cancellationToken)));
    }
}
C#

CompositeAnyNextNotifier クラスのNotifyNextAsync メソッドは、コンストラクタに渡された、INextNotifier インターフェースを実装する別のクラスの内、どれかのNotifyNextAsync メソッドが完了すると、その処理を終了します。つまり、コンストラクタにSpacekeyNextNotifyer クラスとSingleButtonNextNotifier クラスのインスタンスを渡すことで、「スペースキーが押されたとき、もしくはボタンが押されたときにセリフを次に進める」ような機能を実現できます。

ここでは、DIに基づいた機能拡張を行っています。特に、CompositeAnyNextNotifier クラスは、DIにおけるデザインパターンの一つ、コンポジット・パターンを使用しています。

DIに関する詳しい解説はこの記事では行いませんが、この例で重要なのは、各クラスに各機能が独立して存在しているということです。これにより、例えば次のような利点が得られます。

  1. 機能の置き換えがしやすい。例えば、テスト時にのみ有効にしたい機能があるとき、CompositeAnyNextNotifier クラスに渡すインスタンスを切り替えるだけで済む。
  2. 機能の再利用がしやすい。例えば、別のプロジェクトで「スペースキーの機能」のみを再利用したいとき、SpacekeyNextNotifier クラスのみを丸ごと移植するだけで済む。
  3. メンテナンスがしやすい。各機能が各クラスに分散しているので、各クラスの役割が明確になる。また、1クラス当たりのコード量が単純に削減され、見通しが良くなる。

DIの適用により、様々な場面で、システムの拡張性に関して多種多様な利益を得ることができます。そして、DIを適用してシステムを拡張できるのは、そもそもScenarioFlowがDIに基づいて設計されているからです。ScenarioFlowでは、今回の例に限らず、様々な場所でDIに基づいて効率的にシステムの拡張を行うことができ、そのメンテナンス性は高くなります

リフレクションに基づく高いコマンド拡張性

会話シーンの作成をするための会話システムにおいて大部分を占める機能が、コマンドです。コマンドは、シナリオ記述用のスクリプトから呼び出される、C#におけるメソッドのようなものです。コマンドには「キャラクター画像を変更するコマンド」、「音楽を再生するコマンド」などがあり、それらのコマンドを適切に呼び出すことにより、会話シーンが再生されます。なお、ScenarioFlowでは、「セリフを表示する機能」もコマンドに含まれます。

ScenarioFlowにおいて、例えば「キャラクター画像を変更するコマンド」は次のように実装します。

C#
[CommandMethod("change character's image")]
[Snippet("Change the characer {${1:name}}'s image to {${2:image}}.")]
public void ChangeCharacterImage(GameObject character, Sprite sprite)
{
    character.GetComponent<SpriteRenderer>().sprite = sprite;
}
C#

このコマンドは、SFText(シナリオ記述スクリプト)では次のように呼び出すことができます。

SFText
$sync  | change character's image                                | <- コマンド呼び出し
       | Change the characer {Sheena}'s image to {Sheena_Smile}. | 
Sheena | Hello, everyone!                                        | <- これはセリフ
SFText

このSFTextでは、”change character’s image”というコマンドに、”Sheena”、”Sheena_Smile”というパラメータを与えて呼び出しています。

この例にみられる通り、ScenarioFlowではC#のメソッドとコマンドは等価です。言い換えれば、C#のメソッドをそのまま、コマンドとしてシナリオ記述スクリプトから呼び出します。例では、ChangeCharacterImage メソッドが、change character's image というコマンド名でエクスポートされています。

この状況を、奇妙に感じるかもしれません。なぜなら、SFTextに記述されたパラメータは、C#にとってはただの文字列であるのに対し、ChangeCharacterImage メソッドのパラメータの型は、それぞれGameObjectSprite だからです。文字列から、これらのオブジェクトへの変換はどのようにして行われるのでしょうか。

パラメータの変換を行うのは、デコーダーと呼ばれるメソッドです。ScenarioFlowでは、コマンドの引数に使用されるすべての型に対して、パラメータ変換用のデコーダーを用意する必要があります。

今回は、change character's image コマンドを呼び出すためには、GameObject 用のデコーダーとSprite 用のデコーダーを用意する必要があり、それぞれ次のように実装されます。

GameObject用デコーダー
[DecoderMethod]
public GameObject ConvertToGameObject(string input)
{
 	  return GameObject.Find(input);
}
C#
Sprite用デコーダー
[DecoderMethod]
public Sprite ConvertToSprite(string input)
{
	  return Resources.Load<Sprite>(input);
}
C#

ScenarioFlowでは、コマンドが呼び出されると、まずはコマンド名に対応するメソッドが検索されます。そして、その対応するメソッドの引数型が解析され、登録されているデコーダーの中から適切なものが選択され、それを使用して文字列として渡されたパラメータが適切な型のオブジェクトに変換されます。最終的に、変換されたパラメータとともにメソッドが呼び出されます。この一連の処理には、リフレクションが利用されています。

この方式の利点は、コマンドの処理と文字列の変換処理が完全に切り離されることにあります。コマンドの定義側は、文字列がどのように変換されるのかを考える必要はなく、都合のいい引数型を自由に持つことができ、本質的に必要とされる処理のみを記述することができます。また、これはコマンドの処理を記述する際、自然なC#プログラムを書くことができるということを意味します。いちいち文字列の変換処理を気にする必要がないだけでなく、引数型が自然に設定されているので、コマンドとしてエクスポートしたメソッドは、通常のメソッドとしても再利用することができます

自然にプログラムを書くことで新たなコマンドを追加することができるというのは、プログラマーにとっての大きな利益ですが、一方で、その代わりにライターに負担がかかるようなことはありません。論理的に整合性が取れるデコーダーを定義することにより、コマンドを呼び出す側も、直感的にコマンドを使用することができます。むしろ、コマンド追加にかかるコストが減ることにより、気軽に便利なコマンドを追加できるようになるので、結果的にライターの負担も減るでしょう。

このように、ScenarioFlowではリフレクションを利用したコマンド・デコーダ方式により、非常に高いコマンド拡張性を達成しています。

非同期コマンド

change character's image は、同期処理を行う同期メソッドです。ScenarioFlowでは、非同期処理を行う非同期メソッドを非同期コマンドとして呼び出すこともできます。

C#
[CommandMethod("change character's image async")]
[Snippet("Change {${1:name}}'s image to {${2:image}}.")]
public async UniTask ChangeCharacterImageAsync(GameObject character, Sprite sprite, CancellationToken cancellationToken)
{
    // 省略
}
C#
SFText
$parallel | change character's image async                          | <- コマンド呼び出し
          | Change the characer {Sheena}'s image to {Sheena_Smile}. | 
Sheena    | Hello, everyone!                                        | <- これはセリフ
SFText

非同期メソッドには、処理をキャンセルするためのCancellationToken がパラメーターとして渡されます。SFTextの中で、$parallel がそれにあたります。

CancellationToken に対応するパラメーターでは、非同期処理の実行方法を選択します。詳細は別の記事で解説しますが、ここでは「次のコマンドと並列に実行」する方式を指定しています。

SFText:書きやすく読みやすい新たなシナリオ記述スクリプト

SceanrioFlowでは、シナリオ記述用のスクリプトを、作成した会話システムで実行することにより会話シーンが再生されます。SceanrioFlowは、デフォルトのシナリオ記述用スクリプトとしてSFTextを提供しています。「デフォルトの」というのは、ScenarioFlowで使用できるスクリプトはSFTextのみではないという意味です(これについて、詳細はこの記事では取り扱いません)。

SFTextは、「書きやすく読みやすい」をコンセプトとして設計されたスクリプト形式です。本物の台本のような見た目で、単純な文法を持つという特徴を持っています。ここでは簡潔に、その文法と、スクリプトの編集ツールについて紹介します。

簡単な文法

上に、SFTextの一例を示しています。例にみられるように、SFTextではすべての行が縦線によって3つの領域に区切られます。左側と中央には、キャラクター名とセリフ、コマンド名とパラメータなどが、右側にはコメント文が記述されます。

SFTextにおいて行うのは、基本的に「呼び出したい順にコマンドを書く」ことです。

コマンドの呼び出しは、次の形式で行います。

SFText
$TokenCode  | Command Name     | 
            | {Arg1} {Arg2}    | 
            | {Arg3} ...       |
SFText

$TokenCodeは、トークンコードと呼ばれるパラメータの一種です。トークンコードでは、非同期コマンドを呼び出す際の、その実行方法を選択します。指定するトークンコードによって、次のコマンドと同時に実行したり、処理の中断を許可または禁止にしたりできます。ただの同期コマンドを呼び出す場合は、$syncを指定します。

また、コマンド呼び出しに限らず、SFTextにおいてパラメータを記述する際は{}で囲まれた部分のみがパラメータと認識され、その外に書かれたテキストはすべてコメントとして認識されます。

ScenarioFlowでは、セリフの表示もコマンドで行います。セリフの表示は、会話シーンの中では頻出の処理なので、SFTextではセリフを表示するコマンドのための糖衣構文(より簡単な記述法)が用意されています。

SFText
#command       | {Command A}    | <- コマンド名を指定
#token         | {$TokenX}      | <- トークンコードを指定
Character Name | Text1          | <- 話者名と、セリフの内容
               | Text2          | 
               | Text3          |
SFText

#label を利用して、あるブロックをラベル付けしてシナリオ分岐を実装することもできます。

SFText
#label | {Label Name} | <- ラベル名を指定
SFText

他にも、セリフのブロックに追加のパラメータを指定できたり、各行の右側にはコメントをかけたりといった機能があるのですが、上記の文法が基本になります。SFTextでは、この文法に則ってセリフとコマンドを上から並べることで、会話シーンを実現します。

なお、SFText(というよりScenarioFlowのシナリオ記述)においては、非同期コマンドに指定するトークンコードの違いを理解することが最も重要なのですが、その解説はまた別の機会に持ち越すことにします。

VSCode拡張機能による入力支援

SFTextを快適に編集するため、Visual Studio Code (VSCode) 向けの拡張機能を使用することができます。

SFText Extension Pack - Visual Studio Marketplace
Extension for Visual Studio Code - Extensions for SFText

この拡張機能は、シンタックスハイライトと入力補完を提供しています。

まとめ

ScenarioFlowは、会話シーンを作成するための会話システムを作成するためのライブラリです。あなたのプロジェクトに合わせた、独自の会話システムを実装することができます。現在、Unity公式のアセットストアでダウンロードができます。

ScenarioFlow | Integration | Unity Asset Store
Use the ScenarioFlow from .PROLOGUE on your next project. Find this integration tool & more on the Unity Asset Store.

ScenarioFlowの大きな長所は、自由度が高く、システムの拡張性が高いことです。このライブラリはDIを基に設計されており、システム全体としての柔軟性が高く、リフレクションを基にしたコマンド・デコーダー方式により、コマンド拡張性が非常に高くなっています。特にコマンド拡張の方法は革命的で、ScenarioFlowではC#のメソッドをそのままコマンドとして呼び出すことができ、パラメーターの型も自由です。自然なC#プログラムを書くことで、コマンドを拡張できます。

また、ScenarioFlowは新たなシナリオ記述スクリプト、SFTextを提供します。SFTextは、単純な文法と、本物の台本のような見た目を持ち、「書きやすく読みやすい」をコンセプトに設計されています。SFTextは、VSCodeのプラグインにより、快適に編集することができます。

ScenarioFlowは、独自の機能を備えた会話シーンを作成したかったり、最小限の機能でコンパクトな実装をしたかったりするときなどに最適です。あなたのプロジェクトに最適な、拡張性の高い会話システムを実装することができるでしょう。

この記事では、ScenarioFlowの特徴について簡潔に紹介しました。別の記事ではScenarioFlowの使い方について、より詳細に解説していきます。お楽しみに!

コメント