【Unity】VitalRouter.MRubyでアドベンチャーゲームの会話機能を作ってみた

# 何の記事？

hadashiA さんが公開されている [VitalRouter.MRuby](https://vitalrouter.hadashikick.jp/extensions/mruby) というライブラリを使って、
<font color="orange">アドベンチャーゲームを作るための基礎的な仕組みを制作</font>してみました！
用意した機能は以下の四点です。

- テキストフィールドに文字を表示し、ユーザーの文字送りを待つ機能
- 選択肢を表示し、ユーザーの選択を待つ機能
- 選択結果によってシナリオを分岐させる方法
- ノード形式でシナリオを管理する記法

今回はこれらの解説記事となります。

:::note
[Lua-Csharpを使って同等の機能を実装した記事も公開しています。](https://qiita.com/amenone_games/items/7fd5ff97df22988cc955)
同じサンプルプロジェクトで両方確認できますので、ぜひ両方試してみてください。
:::

# VitalRouterとは

VitalRouter はUniRxやMessagePipeのような <font color="orange">メッセージングライブラリ</font> の一つです！

structを使った ゼロアロケーションなメッセージングが可能です。
また、Source Generater を使った簡単な Subscribe の記法が提供されており、
慣れてくると簡単にSubscribeができます！

# VitalRouter.MRubyとは

VitalRouter.MRuby は、<font color="orange">Unity上でMRubyのスクリプトを実行可能にする</font>ライブラリです。
また、<font color="orange">MRubyスクリプト上からVitalRouterのメッセージ発行ができる</font>ようになっています。

近しいコンセプトのライブラリにluaの実行を可能にするものがいくつかありますが、
このライブラリは MRubyからC#への接続をメッセージの発行に絞っています。
そのため
- 簡単にメッセージの発行が可能
- MRubyとC#の権限の切り分けが明示的

という利点があります。

::: note
MRuby は、組み込みシステム向けに開発された、Rubyの派生言語です。
:::

# サンプルレポジトリ

とにかく動かしたいという方は以下のレポジトリをCloneやDLしてみてください。
Assets/sample シーンを開いて実行すると
Assets/Resources/Ruby/main.rb の中身が実行されます。

https://github.com/amenonegames/UnityRubyAdv

以下ではこのレポジトリの中身を例に
- MRubyの構成
- Scriptの構成
- Unityの構成

の順に解説していきます！


# MRubyの構成
Assets/Resources/Ruby/main.rb は

 - メソッド定義
 - DialogueManager定義
 - テキスト本体

に分かれています。

▼全文はこちらからご覧ください！

https://github.com/amenonegames/UnityRubyAdv/blob/main/Assets/Resources/Ruby/main.rb

## メソッド定義
VitalRouter.MRubyからのメッセージ発行は、以下の書式で提供されます。

```ruby
cmd :コマンド名, プロパティ名1: 123, プロパティ名2: "bra bra"  
```

アドベンチャーのスクリプトを作って行くにあたって、
毎回この記法で書くのは大変です。
今回は、最初に省略記法を定義しました。

```ruby
##### メソッド定義 #####

### デバックコマンドの発行を 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
```

このように定義することで、
<font color = "orange">次のような記法でテキストフィールドへの文字更新と、
ユーザーの文字送りボタン待機を実現しています。</font>

```ruby
t "今からサンプルADVを初めていくよ！"
```

▼ 動かしているところを動画に撮影しました ▼

![MRuby2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/c70757c9-fa2a-3b2f-ec99-f7b213a508f2.gif)


## DialogueManager定義

ADVを作るにあたって選択肢分岐はつきものです。
そこで、<font color ="orange">ユーザーの選択結果によって node を指定して jump できる記法</font>が必要になります。

これを可能にするために、```DialogueManager```を MRuby上で実装しました

```ruby
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 を使ってスクリプトを記載することができます。

```ruby
dm = DialogueManager.new

dm.add_node(:first) do
    t "初めに実行されるノード"
    dm.jump(:second)
end

dm.add_node(:second) do
    t "次に実行されるノード"
end

# ダイアログの開始
dm.jump(:start)
```

::: note 
メソッドを順に実行するように書けば、通常の記法でもjump自体の実装は可能です。

ただし、MRuby ではメソッド呼び出しより先にメソッド定義をしなければならないため、通常の記法では 分岐処理の後に分岐後の実装を書くことができません。
これでは取り回しが悪いため、DialogueManager を実装しました。
:::

## テキスト本体
テキストは上記の組み合わせで出来ています。

選択肢の分岐処理に少し癖があるので追記しておきます。

[上で定義](https://qiita.com/amenone_games/items/74ff00005f29114f8d2f#%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E5%AE%9A%E7%BE%A9)した o メソッドを使うことで、
以下のように o の後に配列を渡すと選択肢が表示されます。
```ruby
o ["Yes", "No"]
```
実行時には以下のように右側に選択肢が現れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/2162b8be-4663-b3e7-2b5a-5869119f7bfa.png)

ユーザーが選択した結果は、``state[:result]`` の中に格納されます。
```result = state[:result].is?("Yes")``` のように、格納された文字列を比較チェックして分岐処理を書けます。

```ruby
o ["Yes", "No"] ##選択肢を表示し、選択されるまで待機
result = state[:result].is?("Yes") ##選ばれた結果を変数に格納
if result ## trueが入っているかどうかで分岐
    t "Yes? Really?"
else 
    t "No? Really?"
end
```

:::note
今回は、bool値としてresultの中に変数格納していますが、
if の後に比較文を書いて 三択以上の分岐に対応することも可能です。
:::

ここまでがMRubyスクリプトの内容となります。


# Scriptの構成

MRuby上からメッセージ発行を可能にする記法と、
メッセージをSubscribeする記法を見ていきます。
また、MRubyを実行する方法についても見ていきます。

## メッセージオブジェクトの用意

VitalRouterでは ```ICommand```を実装したclassやstructなどを定義することでメッセージ用のオブジェクトを作ることができます。

通常のVitalRouterを使用する場合は、これだけでメッセージオブジェクト宣言が完了します。
```csharp
    public struct TalkCommand : ICommand
    {
        public string Message;
    }
```

今回は MRuby 側からこのメッセージを呼び出せるように、更に変更を加えます。

### メッセージオブジェクトへの変更

メッセージオブジェクトにMRubyObjectアトリビュートを付け、partialにします。
```csharp
    [MRubyObject]
    public partial struct TalkCommand : ICommand
    {
        public string Message;
    }
```

### CommandPresetの宣言
MRubyCommandPresetを継承したpartialクラスを宣言し、MRubyCommandアトリビュートをつけます。

```csharp
    [MRubyCommand("talk", typeof(TalkCommand))]
    public partial class MyCommandPreset : MRubyCommandPreset { }
```
上の``` talk ```にあたる部分が、MRuby側で実行するコマンド名になります。
```typeof(TalkCommand)```の部分には、作成したメッセージオブジェクトを記載してください。

:::note
発行可能メッセージを追加する場合は、このクラスにアトリビュートを増やします。
[サンプルプロジェクトのMyCommandPreset](https://github.com/amenonegames/UnityRubyAdv/blob/main/Assets/Scripts/Logic/Ruby/CommandPreset/MyCommandPreset.cs
)では```debug``` ```talk``` ```option``` の３つのコマンドを発行可能にしています。

:::

### CommandPresetの登録
```MyCommandPreset```を```MRubyContext```に登録します

```MyCommandPreset```を new や inject し、```MRubyContext```に登録します。
```MRubyContext```については後述します。

```csharp
context.CommandPreset = _mycommandPreset; 
```

::: note
サンプルプロジェクトでは、[RubyRunner](https://github.com/amenonegames/UnityRubyAdv/blob/main/Assets/Scripts/Logic/Ruby/RubyRunner.cs)上で登録を行っています。
:::

ここまでで、メッセージの宣言は完了です。
## メッセージのSubscribe

Subscribeは、通常のVitalRouterの記法と同じですが、せっかくなので解説していきます。
今回使っているのはVContainerを利用した登録方法です。

### Presenterの用意

Presenterをpartialクラスで用意し、クラスに```Routes```アトリビュートをつけます。
メッセージオブジェクトを引数に取るメソッドを定義し、```Route```アトリビュートを付けます。
以下のようなクラスになります。

```csharp
    [Routes]
    public partial class TalkPresenter
    {
        //asyncメソッドとして登録する場合
        [Route]
        async UniTask OnTalk(TalkCommand command)
        {
            //テキスト表示の処理を書く
        }

        // 仮にsyncメソッドにしたい場合は以下のように書きます
        // [Route]
        // void OnTalk(TalkCommand command)  { //必要な処理 }
    }
```
これだけでSubscribeできるのは画期的ですね！！

:::note
サンプルプロジェクトの [TalkPresenter](https://github.com/amenonegames/UnityRubyAdv/blob/main/Assets/Scripts/Presenter/TalkPresenter.cs) で購読処理を行っているので、必要に応じて参照してください。
:::

### Containerへの登録

この Presenter をコンテナに登録していきます。
通常の ```Register``` とは異なり、```RegisterVitalRouter```というメソッドの中に
```Action<RoutingBuilder>```として登録します。

登録には```RoutingBuilder.Map<T>```を利用します。
つまり、以下のようなコードになります。

```csharp
public class SampleContainerBuilder : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.RegisterVitalRouter( routing =>
        {
            routing.Map<TalkPresenter>();
        });
    }
}
```

これで、メッセージの購読処理は完了です。

:::note
サンプルプロジェクトの[コンテナ](https://github.com/amenonegames/UnityRubyAdv/blob/main/Assets/Scripts/SampleContainerBuilder.cs)でも同様の登録をしています。

:::

## MRubyの実行

MRubyを実行するには、
1. MRubyContextを生成し、
1. CommandPresetとRouterを登録し、
2. MRuby本体の```string```、または ```byte[]```を渡してコンパイルを走らせ、
3. 実行する

必要があります。
コードにすると以下の通りです。

```csharp
//コンテキストの生成
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への登録](https://qiita.com/amenone_games/items/74ff00005f29114f8d2f#container%E3%81%B8%E3%81%AE%E7%99%BB%E9%8C%B2)のステップですでに登録されています。Injectして```Context```に登録してください。

:::note
サンプルプロジェクトでは [RubyRunner](https://github.com/amenonegames/UnityRubyAdv/blob/main/Assets/Scripts/Logic/Ruby/RubyRunner.cs) 上で登録作業を行っています。
:::

:::note warn
公式サイト上は ```Router.Default``` を```Router```として登録するコード例もありますが、VContainerを使用する場合は Containerに自動登録されているRouterを利用してください。
:::

## 変数へのアクセス
C#からMRubyと同じ変数にアクセスするためには、MRubyContextを利用します。

### C#側からのアクセス
C#側からは以下のようにアクセスします。

```csharp
//値を指定の変数名でSetする場合
Context.SharedState.Set("変数名","値");

//指定の変数名の値をGetする場合
Context.SharedState.GetOrDefault<T>(key);
```

:::note
サンプルプロジェクトでは[RubySharedStateHandler](https://github.com/amenonegames/UnityRubyAdv/blob/main/Assets/Scripts/Logic/Ruby/RubySharedStateHandler.cs)からアクセスしています。
:::
## MRubyからのアクセス

MRubyからは、以下の書式でアクセス可能です。
```ruby
## 値を取り出すとき
state[:変数名]

## 比較するとき
state[:変数名].is?('Yes')
```

:::note
サンプルプロジェクトでの選択肢の分岐処理は、この共通変数に値を格納することで行っています。
詳細は[OptionPresenter](https://github.com/amenonegames/UnityRubyAdv/blob/main/Assets/Scripts/Presenter/OptionPresenter.cs)をご覧ください。
:::
# Unityの構成
Containerにほぼ全ての機能登録が集約しています。
選択肢のプレファブやテキストの表示登録はこちらにありますので、必要に応じて参照してください。

また、ContainerのUseRubyのチェックを外すと、Assets/Resources/Lua/main.lua が実行されます。
こちらの詳細については[別記事](https://qiita.com/amenone_games/items/7fd5ff97df22988cc955#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB)で解説しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/ca43e6f8-8296-a866-ab6e-0cf0a48f25e7.png)

# おわりに
以上で VitalRouter.MRuby を使ってアドベンチャー用のスクリプト環境を構築できました！

また、このサンプルはもともと Ruby と Lua の違いを確かめたく作ったものですので、
ContainerのUseRubyのチェックを外すだけで Assets/Resources/Lua/main.lua が実行できます。

色々と試してみてご自身のプロジェクトに合う環境を探してみてください！

# その他のリソース

▼公式ページ▼

https://vitalrouter.hadashikick.jp/extensions/mruby

▼作者様による解説記事▼

https://scrapbox.io/hadashiA/VitalRouter.MRuby_-_Unity%E5%90%91%E3%81%91%E6%B1%8E%E7%94%A8Ruby%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF