【Unity】Lua-CSharpを使ってアドベンチャーゲームの会話機能を作ってみた

# 何の記事？
AnnulusGames さんが公開されている[Lua-CSharp](https://github.com/AnnulusGames/Lua-CSharp)というライブラリを使って、
<font color="orange">アドベンチャーゲームを作るための基礎的な仕組みを制作</font>してみました！
用意した機能は以下の四点です。

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

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

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

# Lua-CSharpとは
Lua-CSharpはUnity上でLuaを実行するためのライブラリです！

Lua実行ライブラリは [MoonSharp](https://www.moonsharp.org/)や[xLua](https://github.com/Tencent/xLua)など、様々ありますが、Lua-CSharpは以下のような特徴があります！

- 実行速度が速い
- async awaitに対応
- 拡張パッケージで .lua 形式ファイルを直接読み込める

特にasync awaitに対応している点が非常に強力で、
(私が調べた限り)他のライブラリない強力な利点だと思います！

▼公式日本語リファレンス▼

https://github.com/AnnulusGames/Lua-CSharp/blob/main/README_JA.md#luaobject

# サンプルレポジトリ
とにかく動かしたいという方は以下のレポジトリをCloneやDLしてみてください。

https://github.com/amenonegames/UnityRubyAdv/tree/main

Assets/sample シーンを開き、<font color = "orange">```Container``` の UseRuby のチェックを外して</font> 実行してください。

![container.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/51b2c8b3-017e-b504-e506-9bf9fb9bd3cb.png)

[Assets/Resources/Lua/main.lua の中身](https://github.com/amenonegames/UnityRubyAdv/blob/main/Assets/Resources/Lua/main.lua) が実行されます。


以下ではこのレポジトリの中身を例に

- Luaの構成
- Scriptの構成
- Unityの構成

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

# Luaの構成

```main.lua```は ```commands.lua``` と ```DialogueManager.lua``` を読み込んでいます。
以下ではそれぞれの lua の内容について解説していきます。

## commands.lua の内容


このプロジェクトでは、Luaファイル上で、C#のメソッドを実行できるようにしてあります。
コマンド名は```print```と```talk```と```option```です。

```talk```と```option```は会話を書く上で頻出するため、省略記法を定義しています。
その定義ファイルが```commands.lua```です。

```lua:commands.lua
﻿-- モジュールテーブルを作成
local helpers = {}

-- t 関数を定義
helpers.t = function(message)
    talk(message)
end

-- o 関数を定義
helpers.o = function(options)
    return option(options)
end

-- モジュールを返す
return helpers
```

このように定義し、更にmain.lua上で、次のように定義して ```t``` と ```o``` をfunctionとして定義しています。

```lua:main.lua
local helpers = require ("Assets/Resources/Lua/commands.lua") --commandsの中身をhelpersに格納
local t = helpers.t --helpersのt関数を t と再定義
local o = helpers.o --helpersのo関数を o と再定義
```

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

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

▼ 動かしているところを動画に撮影しました ▼
![Lua.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/fdb08e27-b63f-f8c5-53bd-9487e697adf5.gif)

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

これを可能にするため、DialogueManagerを Lua上で実装しました。
```lua:DialogueManager.lua
local DialogueManager = {}

--nodesテーブルを用意
DialogueManager.nodes = {} 
--標準のstartNode名を定義
DialogueManager.startNode = "start"

-- 引数のfuncをnodesテーブルに追加
function DialogueManager:addNode(nodeName , func)
   self.nodes[nodeName] = func
end

-- jump指定されたノード名でnodesを検索し、関数を呼び出す
function DialogueManager:jump(nodeName)
    if self.nodes[nodeName] then
        self.nodes[nodeName]() -- 関数を呼び出す
    else
        print("Node not found!")
    end
end

function DialogueManager:start()
    self:jump(self.startNode)
end

function DialogueManager:setStartNode(nodeName)
    self.startNode = nodeName
end

return DialogueManager
```

また、```main.lua```で次のように定義することで```addNode```と```jump```を直接記入できるようにしています。

```lua:main.lua
-- DialogueManagerをdmに格納
local dm = require ("Assets/Resources/Lua/DialogueManager.lua")

-- dm内のaddNodeを addNode だけで呼び出せるように登録
local addNode = function(...) dm:addNode(...) end
-- dm内のjumpを jump だけで呼び出せるように登録
local jump = function(...) dm:jump(...) end
```

このように定義しておくことで、
次のように<font color = "orange"> ```addNode``` と ```jump``` を使って会話スクリプトを記載</font>することができます。

```lua
addNode 
("first", function()
    t "初めに実行されるノード"
    jump "second"
end)

addNode 
("second", function()
    t "次に実行されるノード"
end)

-- ダイアログの開始
jump "first"
```

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

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

## main.lua の内容

```main.lua```の会話スクリプトは上記の組み合わせで出来ています。

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

上で定義した o メソッドを使うことで、
以下のように o の後に配列を渡すと選択肢が表示されます。

```lua
local result = o { "Yes", "No" }
```
実行時には以下のように右側に選択肢が現れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/feb40413-1f69-6496-55c2-7877f493ff8a.png)

選択すると、戻り値である result にユーザーの選択結果が入ってきます。
以下のように<font color="orange"> result の内容を比較して、分岐処理を書くことができます！</font>

```lua
    local result = o { "Yes", "No" }
    if result == "Yes" then
        jump "yesResponse"
    else
        jump "noResponse"
    end
```

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

# C# Scriptの構成
続いてC#の実装内容を解説していきます。
ここでは、

- C#からLuaを実行する方法と
- C#とLuaで共通変数を扱う方法

について解説していきます。

## C#からLuaを実行する方法

Luaを実行するには、以下の手順を踏みます。
1. LuaStateを生成し
1. 必要なコマンドやモジュールを登録し
1. ファイルを実行する

コードにすると以下の通りです。

```csharp
//LuaStateを生成
var state = LuaState.Create();
//メソッドを登録
state.Environment["メソッド名"]=(new LuaFunction((context , buffer, token) =>
            {
                //メソッドの処理
            }));
//モジュールを登録
state.OpenModuleLibrary();
//ファイルを実行
state.DoFileAsync("Assets/Resources/Lua/main.lua", cancellationToken:token);
```

### コマンドの登録

コマンドは```LuaState.Environment```のテーブル内に```LuaFunction```として登録できます。
書式は以下の通りです。

```csharp
state.Environment["メソッド名"]=(new LuaFunction((context , buffer, token) =>
            {
                //メソッドの処理
            }));
```

Lua内で "メソッド名" で登録した名前を呼ぶことでC#メソッドを実行できます。

:::note
サンプルプロジェクト上は[LuaFunctionAdder](https://github.com/amenonegames/UnityRubyAdv/blob/main/Assets/Scripts/Logic/Lua/LuaFunctionAdder.cs)でコマンドを登録しています。
:::

---
ここからは、登録時の仕様について、
- Luaからの引数/返り値の利用
- syncメソッドとasyncメソッド

　の順に解説します。

#### Luaからの引数/返り値の利用
Luaから与えられた引数は、Actionの引数になっている```context``` から取り出すことができます。
また、Luaに返り値を与える場合は、```buffer```と```LuaFunction```の戻り値を使います。

```csharp
state.Environment["add"] = new LuaFunction((context, buffer, ct) =>
{
    // context.GetArgument<T>()で引数を取得
    var arg0 = context.GetArgument<double>(0);
    var arg1 = context.GetArgument<double>(1);

    // Luaに返り値を返す場合、直接 LuaFunction の返り値は使いません。
    // 戻り値は引数の buffer に格納してください。
    buffer.Span[0] = arg0 + arg1;

    // 戻り値の数を返します
    return new ValueTask<int>(1);
});

```

#### syncとasync
```LuaFunction```の返り値は```ValueTask<int>>```なので、
asyncキーワードをつけるだけでasyncメソッドになります。
以下のように利用できます。

```csharp
state.Environment["wait"] = new LuaFunction( async (context, buffer, ct) =>
{
    // context.GetArgument<T>()で引数を取得
    var arg0 = context.GetArgument<double>(0);

    await UniTask.WaitForSeconds( arg0 , cancellationToken:token);

    // 戻り値の数を返す
    return 0;
});
```

コマンドの登録については以上です。

:::note
コマンドの登録はより記述量の少ない [[LuaObject]を利用する方法](
https://github.com/AnnulusGames/Lua-CSharp/blob/main/README_JA.md#luaobject)が公式で推奨されています。
今回は、C#側でInjectされたメンバ変数を利用したかったため、Environment経由の登録を使いました。
:::

### モジュールの登録

Lua-CSharpでは、Luaの標準ライブラリの一部を選択して読み込めるようになっています。
LuaStateへの拡張メソッドとして提供されているので、```OpenLibsExtensions```を確認してみてください。

ここまでが、コマンドとモジュールの登録方法になります。

:::note
サンプルプロジェクトでは、[LuaRunner](https://github.com/amenonegames/UnityRubyAdv/blob/main/Assets/Scripts/Logic/Lua/LuaRunner.cs)内で```LuaState.OpenModuleLibrary();```を行っています。
このライブラリを読み込むことで、<font color ="orange">Lua内で ```require``` が宣言できる</font>ようになります。
:::

## C#とLuaで共通変数を扱う方法
<font color ="orange">C#とLuaで共通の変数を扱う</font>ことも可能です。
この場合も```LuaState```を使います。

以下のようなコードでgetやsetが可能です。

```csharp
//setする方法
LuaState.Environment["変数名"] = "任意の値";

//getする方法
LuaState.Environment["変数名"].Read<T>();

// LuaStateはLuaファイルを実行しているインスタンスを利用してください。
```

:::note
サンプルプロジェクトでは、[LuaSharedStateHandler](https://github.com/amenonegames/UnityRubyAdv/blob/main/Assets/Scripts/Logic/Lua/LuaSharedStateHandler.cs)で共通変数にアクセスしています。
:::

## その他のスクリプトの概観

サンプルプロジェクトは[VitalRouter](https://github.com/hadashiA/VitalRouter)と[VContainer](https://vcontainer.hadashikick.jp/ja/)を利用しています。

[LuaFunctionAdder](https://github.com/amenonegames/UnityRubyAdv/blob/main/Assets/Scripts/Logic/Lua/LuaFunctionAdder.cs)で登録したコマンドは、```VitalRouter``` のメッセージを発行しており、その完了待機をしています。

実際の処理は、[TalkPresenter](https://github.com/amenonegames/UnityRubyAdv/blob/main/Assets/Scripts/Presenter/TalkPresenter.cs)などのPresenter側から辿ったほうが理解しやすいかと思いますので、必要に応じてご参照ください。

:::note
その他のPresenterも[同じフォルダ](https://github.com/amenonegames/UnityRubyAdv/tree/main/Assets/Scripts/Presenter)にまとめてあります。
:::

:::note
メッセージをPublishした際、Presenter内の「Routeアトリビュートがついていて引数の型が一致するメソッド」が実行されます。
:::

# Unityの構成
Containerにほぼ全ての機能登録が集約しています。
選択肢のプレファブやテキストの表示登録はこちらにありますので、必要に応じて参照してください。

また、ContainerのUseRubyのチェックをつけると、Assets/Resources/Ruby/main.rb が実行されます。
こちらの詳細については[別記事](https://qiita.com/amenone_games/items/74ff00005f29114f8d2f)で解説しています。

![container.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/51b2c8b3-017e-b504-e506-9bf9fb9bd3cb.png)

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

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

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