【Unity】会話シーン実装ライブラリ"ScenarioFlow"の紹介
はじめに
こんにちは。伊都アキラと申します。
私は普段、ゲームエンジンのUnityとC#を中心に取り扱って活動しています。
この度、私が手掛けたUnity用のライブラリである"ScenarioFlow"が、Unity公式アセットストアでリリースされました!
ScenarioFlowは、ゲームにおける会話シーン(キャラクター同士が会話していたり、プレイヤーが選択肢を選んでシナリオ分岐したりするやつ)を効率的に実装するためのライブラリです。ゲームエンジンのUnityで会話シーンを作成する際、プログラマー、ライターの仕事量がともに削減されるような、様々な仕組みを提供しています。
本記事は、このライブラリについての簡単な紹介になります。
興味がわいたら、ぜひアセットストアにてダウンロードしてみていただけると嬉しいです。
(無料で配布しています!)
ScenarioFlowとは?
ScenarioFlowは、ゲームエンジンのUnityで利用できる、会話シーンを効率的に実装するためのライブラリです。
現在、Unity公式アセットストアにて公開されており、無料で利用可能です。
このライブラリは、プログラマー向けには拡張性の高いシステムを、ライター向けにはシナリオ記述用のスクリプトを提供しています。それらを用いて、ライターはシナリオを、プログラマーはそれを画面上で動かすためのシステムを、それぞれ効率的に作成することができます。
Asset Storeのキーイメージより
Asset Storeのキーイメージより
ところで、Unityで使える、会話シーンを作成することを目的としたライブラリはすでにいくつかあります。例えば、宴やNaninovelなどが有名です。
しかし、ScenarioFlowはこれらのライブラリとは全く異なる思想とアーキテクチャを持っています。
ここからは、ScenarioFlowの特徴を思想とアーキテクチャ、二つの観点から紹介していきます。
SceanrioFlowの思想
SceanrioFlowが目指しているのは、「会話シーンを必要とするすべてのプロジェクトに対応すること」です。
先に挙げたような既存のライブラリは、基本的に「プログラムの知識があまりなくても、簡単にノベルゲームを作ることができる」といったようなことを謳っていて、実際にそれが長所になっています。
豊富な機能を備えたシステムによって、ユーザーはプログラムをほとんど書くことなく、簡単に会話シーンを作ることができます。
しかし、そのようなシステムは拡張や変更に対して硬いです。
それは、多くの機能を備えたことによるシステムの肥大化と、処理のフローの複雑化が原因です。
一方のScenarioFlowは、会話シーンの作成における本質的な機能のみを提供し、アーキテクチャを工夫することによって、処理フローの単純化及び自由度の大幅な向上を図っています。
ここで、会話シーンの実現に必要な機能を見てみましょう。大まかに分けて、以下の4つが挙げられます。
-
進行制御
- シナリオ進行を司る
- 例えば、「画面をタップすると次のセリフに進む」、「『オートボタン』を押すと、自動でシナリオが進行する」など
-
スクリプト解析
- スクリプト:セリフや実行する演出が書かれた台本
- 定められた文法に従ってスクリプトを解釈し、プログラムで実行可能な形式に変換する
-
演出
- スクリプト側から呼び出される機能
- 「セリフの表示」、「キャラクター画像変更」、「音楽再生」など
-
統合
- 1~3のプログラム群を監督して、一つのシステムとして稼働させる
上に挙げたような、会話シーンを実現するためのプログラム群を、ScenarioFlowでは「会話システム」と呼んでいます。重要なのは、各ライブラリが会話システムに必要な機能のうちでどこまでを提供しているのかということです。
既存のライブラリは、機能1~4のすべてを提供します。そのため、ほぼ追加のコードを書くことなく、会話シーンを作り始められます。
ScenarioFlowが提供するのは、機能2と4、そして機能1については一部を提供します。機能3については、一切提供していません。機能3を持たないというのは、他のライブラリとの思想の差が顕著に表れていて、例えばセリフを表示するためのプログラムすら、ScenarioFlowではユーザーが書く必要があります。
これは、特に機能3のようなプログラムは、プロジェクトに依存して要求が変わる可能性が高いためです。セリフの表示という演出すら、何を入力として、どのようにテキストを表示するかといった視覚的なことや、実行によりシステム内部の状態にどのような作用を引き起こすかといった内部的なことについて、何が求められるのかはプロジェクトによって変わりえます。
結局、ScenarioFlowの思想とは、プロジェクトに依存しない機能はライブラリが、プロジェクトに依存する機能はユーザーが実装するべきということです。
「会話シーンにおける本質的な機能」と「プロジェクトに依存して要求が変わる機能」をうまく切り離し、前者のみを提供することで、ライブラリはどのようなプロジェクトにも対応することができるようになります。そして、後者についてはユーザーがある程度のプログラムを書く必要がある一方で、実現したい機能や演出に対する自由度が大幅に向上します。
ScenarioFlowは、すべてのプロジェクトに共通して必要な機能と、プロジェクトに依存する機能をうまくシステムに組み込むためのシステムを提供しています。
ある程度の知識を持った開発者であれば、自身のプロジェクトに合った会話システムを効率的に実装することができるでしょう。
ScenarioFlowのアーキテクチャ
ScenarioFlowにおいて、会話シーンを実行するまでの流れは以下の通りです。
- シナリオスクリプトを書く
- シナリオスクリプトをC#で実行可能なシナリオブックに変換する
- シナリオブックを実行する
ScenarioFlowでは、セリフや演出などの情報が記された台本のことを、C#スクリプトと区別するために「シナリオスクリプト」と呼ぶことがあります。
シナリオスクリプトはUnityにScenarioScript
クラスとして認識され、それをC#で実行可能な形式である「シナリオブック」(ScenarioBook
クラス)に変換することで、会話シーンが実行できるようになります。
なお、シナリオスクリプトを実行するにあたり、プログラマーは最低限「コマンド」と「進行制御」のプログラムを実装しておく必要があります。
今回は、シナリオスクリプトとコマンドの二つに焦点を当てて、ScenarioFlowで会話シーンが動く仕組みと、その長所についてみていきます。
シナリオスクリプト
シナリオスクリプトは、キャラクターのセリフや実行する演出の情報などを記した台本です。ScenarioFlowでは、シナリオスクリプトを作成し、それをC#で実行可能なシナリオブックに変換し、それを実行することによって会話シーンが再生されます。
現時点で、ScenarioFlowはメインのシナリオスクリプトとしてSFText (ScenarioFlow Text)という形式をデフォルトで提供しています。
SFTextは、「書きやすく読みやすい」をコンセプトにデザインされた、本当の台本のような見た目を持つスクリプトです。文法は単純で覚えやすく、ノンプログラマーにも易しいスクリプトとなっています。
以下に、SFTextの例を示します。意味については次節で少し触れますが、ここでは、このスクリプトでは何をやろうとしているのかを何となく想像してみてください。
SFTextのサンプル
ちなみに、入力補完やシンタックスハイライトなど、Visual Studio Code (VSCode)向けの拡張機能が配布されているので、VSCodeによってSFTextスクリプトを快適に編集することができます。
重要なのは、「SFTextはシナリオスクリプトの一つに過ぎない」ということです。
SFTextは、システムに対してSFTextとしてではなく、ScenarioScriptという抽象クラスとして渡されます。つまり、その抽象クラスを継承していれば、SFText以外でもシナリオスクリプトとして使用することができます。
現時点では、デフォルトのシナリオスクリプトとしてはSFTextスクリプトと、Compositeスクリプトが用意されています。Compositeスクリプトは、複数のスクリプトを一つにまとめるスクリプトです。
今後、他の形式を追加することも予定しており、また、シナリオスクリプトの構造を理解していれば、ユーザー自身で新しい形式を追加することもできます。
将来的には、好みと場面に合わせて複数の形式から適切な一つを選択できるようにしたいと考えています。(もちろん、ほとんどの場面はSFTextで十分なくらい良い出来だとも考えています!)
コマンド
コマンドとは、シナリオスクリプト側から呼び出すことができる、C#でいうところのメソッドのようなものです。他の既存ライブラリも、同様の概念を持っています。
ScenarioFlowにおいては、このコマンドの実装と追加が非常に簡単に、そして効率的にできます。
コマンドがどのようにC#で実装され、どのようにシナリオスクリプト側から呼び出されるのかを見ていきましょう。
まず、サンプルのSFTextです。
SFTextのサンプル
このSFTextは、次のように3つのコマンドを実行することを意味します。
- コマンド"play music"にパラメーター"明るい音楽"、"parallel"を与えて実行
- コマンド"add character"にパラメーター"シーナ"、"(0, -3, 0)"、"シーナ_笑顔"、"serial"を与えて実行
- コマンド"write dialogue"にパラメーター"シーナ"、"こんにちは!<bk>私の名前はシーナ。"、 "standard"を与えて実行(<bk>は改行されたことを表すシンボル)
$(ドルマーク)付きのパラメーターは特別で、コマンドの実行方式を指定します。おおざっぱに言えば、"parallel"なら「次のコマンドと同時に実行」、"serial"なら「次のコマンドと連続実行」、"standard"なら「このコマンドが終了時、ユーザーアクションを待機」のような意味になります。
スクリプトの左側にドルマーク付きのパラメーターが置かれていないブロックは、キャラクターのセリフを意味します。このブロックは、"#command"と"#token"のブロックでそれぞれ指定したコマンド名および、実行方式を指定するドルマーク付きのパラメーターとともに、等価な別のブロックに置き換えられます。シナリオスクリプトにはキャラクターのセリフが多く書かれることは明らかなので、このような省略記法的な構文が用意されています。
重要な点は、ScenarioFlowにおいてすべてのコマンドは統一的に扱われ、会話シーンの再生は、コマンドを順に呼び出すことによって実現されるということです。
では次に、サンプルの中のコマンドがどのようにC#で実装されているのかを見てみましょう。
コマンド"add character"の例です。
// キャラクターのプレハブ
// 透明度の初期値は0で、SpriteRendererコンポーネントを持つ
GameObject characterPrefab;
[CommandMethod("add character")]
public async UniTask AddCharacterAsync(string name, Vector3 pos, Sprite image, CancellationToken cancellationToken)
{
// キャラクターのオブジェクトを生成して配置
var character = GameObject.Instantiate(characterPrefab);
character.name = name;
character.transform.position = pos;
// キャラクターの画像を変更
var renderer = character.GetComponent<SpriteRenderer>();
renderer.sprite = image;
try
{
// 0.5秒かけて透明度を1に
while (renderer.color.a < 1.0f)
{
renderer.color = new Color(1, 1, 1, renderer.color.a + 0.02f);
await UniTask.Delay(TimeSpan.FromSeconds(0.01f));
}
}
finally
{
// キャンセルされたときにも透明度が1であることを保証する
renderer.color = Color.white;
}
}
ポイントは、次の二つです。
- コマンドとC#メソッドは等価であること
- メソッドのパラメーター型は自由であること
まずポイント1について、例のSFTextでコマンド"add character"に与えられたパラメーターとメソッドAddCharacterAsync
のパラメーターの間には、意味論的な対応関係が見られます。
例えば、"(0, -3, 0)"はVector3 pos
に対応しています。
ScenarioFlowでは、コマンドとメソッドは等価です。
ScenarioFlowは、会話シーンの再生において、シナリオスクリプトに書かれた順に対応するC#のメソッドを呼び出しているだけにすぎません。
次にポイント2について、シナリオスクリプトに書かれたコマンドのパラメーターは、C#にとっては文字列として認識されますが、対応するC#メソッドのパラメーター型は自由です。
シナリオスクリプトの文字列は、メソッド呼び出しの前に「デコーダー」と呼ばれる文字列変換メソッドによって変換され、適切な型のオブジェクトとなってメソッドのパラメーターとして使われます。
このデコーダーについては、プログラマーが適切に用意する必要があります。
例えば、Sprite
型用のデコーダーは次のように実装できます。
[DecoderMethod]
public Sprite ConvertToSprite(string input)
{
return Resources.Load<Sprite>(input);
}
ポイント1と2を踏まえて、ScenarioFlowのコマンド実装には次の良い性質があることが導かれます。
- 処理のフローが明確かつ単純になる
- 一つのクラスに複数のコマンドを定義できる
- コマンド追加のために自然なC#プログラムを記述できる
まず一つ目の性質について、結局、シナリオスクリプトの実行では、記述したコマンド名に対応するC#のメソッドが同じく記述されたパラメーターとともに順番に呼び出されるだけです。
これは処理の流れとしては非常に単純であり、その単純さのおかげでプログラマーのコマンド実装も、ライターが使用するシナリオスクリプトの文法も単純になります。
次に2つ目の性質について、ScenarioFlowではコマンドとメソッドは等価です。一つのクラスには複数のメソッドを定義できますから、当然、コマンドも一つのクラスの中で複数定義できることになります。これは、関連するコマンドは同じクラスの中で定義できるということを意味し、その性質はオブジェクト指向言語であるC#ととても相性が良いです。
最後に3つ目の性質について、これが一番の利点かもしれません。
文字列を適切な型に変換する処理をデコーダーというものに任せたおかげで、例として挙げたメソッドAddCharacterAsync
は、パラメーターの型を自由に選択することができ、また、それがどのように変換されて渡されるのかも気にする必要はありません。
何かのメソッドのオーバーライド、もしくは特殊な型の使用を強制されるというわけでもないので、プログラマーはコマンドとして呼び出されるメソッドの処理を、ごく普通のC#のプログラムとして記述することができます。
ScenarioFlowの最大の特徴、同時に長所は、コマンドのロジックが会話シーンのシステムからほぼ完全に切り離されていることです。
厳しい制約を課されることなく、プログラマーは自由にコマンドに関わる処理を記述することができるので、効率的に機能を実装することができるだけでなく、結果としてスケーラビリティの高いシステムを構成することができます。
おわりに
今回は、会話シーンを効率的に実装するためのライブラリ、"ScenarioFlow"の紹介をしました。
ScenairoFlowは既存のライブラリと比較して、拡張性に重きを置いて設計されています。
自身のプロジェクトに会話シーンを組み込みたく、また、システムに独自の機能を多数必要としているという方は、ぜひこのライブラリをお試しください。
効率的に、自分だけの会話システムを実装することができます!
より詳しく知りたい方は、アセットストアのページや、オンラインマニュアルなども参照してみてください!
Discussion
初めまして、ルナと申します。
今製作中のゲームにシナリオを追加したく、色々探した結果ScenarioFlowにたどり着きました。
活用したいと思いオンラインマニュアルを見ていたのですが、サンプルで使われているConsoleSFSampleの中にSFTextが設定されていないように思えます。パッケージの中も探したのですがHideAndSeek.sftxtが見当たらないです。どこか見落としなどあるでしょうか。お手数ですが、教えていただきたいです。
ご質問ありがとうございます。伊都アキラです。
もしかすると、サンプルのリポジトリをクローンしたものを使用されているのかもしれません。もしそうであれば、クローンではなくReleasesにある
ConsoleSFSample.v1.1.0.unitypackage
をダウンロードし、Unityの新規プロジェクトにインポートしてみてください!ConsoleSFSample/Stories/
の中に、スクリプトファイルが入っています。すいません、ご指摘ありがとうございます。無事に進めることができます。