何の記事?
hadashiA さんが公開されている VitalRouter.MRuby というライブラリを使って、
アドベンチャーゲームを作るための基礎的な仕組みを制作してみました!
用意した機能は以下の四点です。
- テキストフィールドに文字を表示し、ユーザーの文字送りを待つ機能
- 選択肢を表示し、ユーザーの選択を待つ機能
- 選択結果によってシナリオを分岐させる方法
- ノード形式でシナリオを管理する記法
今回はこれらの解説記事となります。
Lua-Csharpを使って同等の機能を実装した記事も公開しています。
同じサンプルプロジェクトで両方確認できますので、ぜひ両方試してみてください。
VitalRouterとは
VitalRouter はUniRxやMessagePipeのような メッセージングライブラリ の一つです!
structを使った ゼロアロケーションなメッセージングが可能です。
また、Source Generater を使った簡単な Subscribe の記法が提供されており、
慣れてくると簡単にSubscribeができます!
VitalRouter.MRubyとは
VitalRouter.MRuby は、Unity上でMRubyのスクリプトを実行可能にするライブラリです。
また、MRubyスクリプト上からVitalRouterのメッセージ発行ができるようになっています。
近しいコンセプトのライブラリにluaの実行を可能にするものがいくつかありますが、
このライブラリは MRubyからC#への接続をメッセージの発行に絞っています。
そのため
- 簡単にメッセージの発行が可能
- MRubyとC#の権限の切り分けが明示的
という利点があります。
MRuby は、組み込みシステム向けに開発された、Rubyの派生言語です。
サンプルレポジトリ
とにかく動かしたいという方は以下のレポジトリをCloneやDLしてみてください。
Assets/sample シーンを開いて実行すると
Assets/Resources/Ruby/main.rb の中身が実行されます。
以下ではこのレポジトリの中身を例に
- MRubyの構成
- Scriptの構成
- Unityの構成
の順に解説していきます!
MRubyの構成
Assets/Resources/Ruby/main.rb は
- メソッド定義
- DialogueManager定義
- テキスト本体
に分かれています。
▼全文はこちらからご覧ください!
メソッド定義
VitalRouter.MRubyからのメッセージ発行は、以下の書式で提供されます。
cmd :コマンド名, プロパティ名1: 123, プロパティ名2: "bra bra"
アドベンチャーのスクリプトを作って行くにあたって、
毎回この記法で書くのは大変です。
今回は、最初に省略記法を定義しました。
##### メソッド定義 #####
### デバックコマンドの発行を print と定義
def print(message)
cmd :debug, message:
end
### テキストフィールドへのtalkコマンド発行を t と定義
def t(message)
cmd :talk, message:
end
### 選択肢表示コマンドの発行を o と定義
def o(opMessage)
cmd :option, options: opMessage
end
このように定義することで、
次のような記法でテキストフィールドへの文字更新と、
ユーザーの文字送りボタン待機を実現しています。
t "今からサンプルADVを初めていくよ!"
▼ 動かしているところを動画に撮影しました ▼
DialogueManager定義
ADVを作るにあたって選択肢分岐はつきものです。
そこで、ユーザーの選択結果によって node を指定して jump できる記法が必要になります。
これを可能にするために、DialogueManager
を MRuby上で実装しました
class DialogueManager
## ノードの配列を初期化する
def initialize
@nodes = {}
end
## ノードをメソッドとして@nodes配列に追加する
def add_node(name, &block)
@nodes[name] = block
end
## jump 時に登録されたメソッドを呼び出せるようにする
def jump(name)
if @nodes[name]
@nodes[name].call
else
print "Node #{name} not found!"
end
end
end
このように定義しておくことで、
次のように add_node と jump を使ってスクリプトを記載することができます。
dm = DialogueManager.new
dm.add_node(:first) do
t "初めに実行されるノード"
dm.jump(:second)
end
dm.add_node(:second) do
t "次に実行されるノード"
end
# ダイアログの開始
dm.jump(:start)
メソッドを順に実行するように書けば、通常の記法でもjump自体の実装は可能です。
ただし、MRuby ではメソッド呼び出しより先にメソッド定義をしなければならないため、通常の記法では 分岐処理の後に分岐後の実装を書くことができません。
これでは取り回しが悪いため、DialogueManager を実装しました。
テキスト本体
テキストは上記の組み合わせで出来ています。
選択肢の分岐処理に少し癖があるので追記しておきます。
上で定義した o メソッドを使うことで、
以下のように o の後に配列を渡すと選択肢が表示されます。
o ["Yes", "No"]
実行時には以下のように右側に選択肢が現れます。
ユーザーが選択した結果は、state[:result]
の中に格納されます。
result = state[:result].is?("Yes")
のように、格納された文字列を比較チェックして分岐処理を書けます。
o ["Yes", "No"] ##選択肢を表示し、選択されるまで待機
result = state[:result].is?("Yes") ##選ばれた結果を変数に格納
if result ## trueが入っているかどうかで分岐
t "Yes? Really?"
else
t "No? Really?"
end
今回は、bool値としてresultの中に変数格納していますが、
if の後に比較文を書いて 三択以上の分岐に対応することも可能です。
ここまでがMRubyスクリプトの内容となります。
Scriptの構成
MRuby上からメッセージ発行を可能にする記法と、
メッセージをSubscribeする記法を見ていきます。
また、MRubyを実行する方法についても見ていきます。
メッセージオブジェクトの用意
VitalRouterでは ICommand
を実装したclassやstructなどを定義することでメッセージ用のオブジェクトを作ることができます。
通常のVitalRouterを使用する場合は、これだけでメッセージオブジェクト宣言が完了します。
public struct TalkCommand : ICommand
{
public string Message;
}
今回は MRuby 側からこのメッセージを呼び出せるように、更に変更を加えます。
メッセージオブジェクトへの変更
メッセージオブジェクトにMRubyObjectアトリビュートを付け、partialにします。
[MRubyObject]
public partial struct TalkCommand : ICommand
{
public string Message;
}
CommandPresetの宣言
MRubyCommandPresetを継承したpartialクラスを宣言し、MRubyCommandアトリビュートをつけます。
[MRubyCommand("talk", typeof(TalkCommand))]
public partial class MyCommandPreset : MRubyCommandPreset { }
上のtalk
にあたる部分が、MRuby側で実行するコマンド名になります。
typeof(TalkCommand)
の部分には、作成したメッセージオブジェクトを記載してください。
発行可能メッセージを追加する場合は、このクラスにアトリビュートを増やします。
サンプルプロジェクトのMyCommandPresetではdebug
talk
option
の3つのコマンドを発行可能にしています。
CommandPresetの登録
MyCommandPreset
をMRubyContext
に登録します
MyCommandPreset
を new や inject し、MRubyContext
に登録します。
MRubyContext
については後述します。
context.CommandPreset = _mycommandPreset;
サンプルプロジェクトでは、RubyRunner上で登録を行っています。
ここまでで、メッセージの宣言は完了です。
メッセージのSubscribe
Subscribeは、通常のVitalRouterの記法と同じですが、せっかくなので解説していきます。
今回使っているのはVContainerを利用した登録方法です。
Presenterの用意
Presenterをpartialクラスで用意し、クラスにRoutes
アトリビュートをつけます。
メッセージオブジェクトを引数に取るメソッドを定義し、Route
アトリビュートを付けます。
以下のようなクラスになります。
[Routes]
public partial class TalkPresenter
{
//asyncメソッドとして登録する場合
[Route]
async UniTask OnTalk(TalkCommand command)
{
//テキスト表示の処理を書く
}
// 仮にsyncメソッドにしたい場合は以下のように書きます
// [Route]
// void OnTalk(TalkCommand command) { //必要な処理 }
}
これだけでSubscribeできるのは画期的ですね!!
サンプルプロジェクトの TalkPresenter で購読処理を行っているので、必要に応じて参照してください。
Containerへの登録
この Presenter をコンテナに登録していきます。
通常の Register
とは異なり、RegisterVitalRouter
というメソッドの中に
Action<RoutingBuilder>
として登録します。
登録にはRoutingBuilder.Map<T>
を利用します。
つまり、以下のようなコードになります。
public class SampleContainerBuilder : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
builder.RegisterVitalRouter( routing =>
{
routing.Map<TalkPresenter>();
});
}
}
これで、メッセージの購読処理は完了です。
サンプルプロジェクトのコンテナでも同様の登録をしています。
MRubyの実行
MRubyを実行するには、
- MRubyContextを生成し、
- CommandPresetとRouterを登録し、
- MRuby本体の
string
、またはbyte[]
を渡してコンパイルを走らせ、 - 実行する
必要があります。
コードにすると以下の通りです。
//コンテキストの生成
var context = MRubyContext.Create();
//Routerの登録
context.Router = _router;
//コマンドプリセットの登録
context.CommandPreset = _commandPreset;
//文字列の読み込み
var rubySource = Resources.Load<TextAsset>("Ruby/main");
//MRubyスクリプトとしてのコンパイル
using MRubyScript script = context.CompileScript(rubySource.bytes);
//実行
await script.RunAsync();
CommandPresetは上述した通り、自分で生成したクラスを newするか、Register->Inject して利用します。
また、必要なRouter
は、Containerへの登録のステップですでに登録されています。InjectしてContext
に登録してください。
サンプルプロジェクトでは RubyRunner 上で登録作業を行っています。
公式サイト上は Router.Default
をRouter
として登録するコード例もありますが、VContainerを使用する場合は Containerに自動登録されているRouterを利用してください。
変数へのアクセス
C#からMRubyと同じ変数にアクセスするためには、MRubyContextを利用します。
C#側からのアクセス
C#側からは以下のようにアクセスします。
//値を指定の変数名でSetする場合
Context.SharedState.Set("変数名","値");
//指定の変数名の値をGetする場合
Context.SharedState.GetOrDefault<T>(key);
サンプルプロジェクトではRubySharedStateHandlerからアクセスしています。
MRubyからのアクセス
MRubyからは、以下の書式でアクセス可能です。
## 値を取り出すとき
state[:変数名]
## 比較するとき
state[:変数名].is?('Yes')
サンプルプロジェクトでの選択肢の分岐処理は、この共通変数に値を格納することで行っています。
詳細はOptionPresenterをご覧ください。
Unityの構成
Containerにほぼ全ての機能登録が集約しています。
選択肢のプレファブやテキストの表示登録はこちらにありますので、必要に応じて参照してください。
また、ContainerのUseRubyのチェックを外すと、Assets/Resources/Lua/main.lua が実行されます。
こちらの詳細については別記事で解説しています。
おわりに
以上で VitalRouter.MRuby を使ってアドベンチャー用のスクリプト環境を構築できました!
また、このサンプルはもともと Ruby と Lua の違いを確かめたく作ったものですので、
ContainerのUseRubyのチェックを外すだけで Assets/Resources/Lua/main.lua が実行できます。
色々と試してみてご自身のプロジェクトに合う環境を探してみてください!
その他のリソース
▼公式ページ▼
▼作者様による解説記事▼