何の記事?
AnnulusGames さんが公開されているLua-CSharpというライブラリを使って、
アドベンチャーゲームを作るための基礎的な仕組みを制作してみました!
用意した機能は以下の四点です。
- テキストフィールドに文字を表示し、ユーザーの文字送りを待つ機能
- 選択肢を表示し、ユーザーの選択を待つ機能
- 選択結果によってシナリオを分岐させる方法
- ノード形式でシナリオを管理する記法
今回はこれらの解説記事となります。
VitalRouter.MRubyを使って同等の機能を実装した記事も公開しています。
同じサンプルプロジェクトで両方確認できますので、ぜひ両方試してみてください。
Lua-CSharpとは
Lua-CSharpはUnity上でLuaを実行するためのライブラリです!
Lua実行ライブラリは MoonSharpやxLuaなど、様々ありますが、Lua-CSharpは以下のような特徴があります!
- 実行速度が速い
- async awaitに対応
- 拡張パッケージで .lua 形式ファイルを直接読み込める
特にasync awaitに対応している点が非常に強力で、
(私が調べた限り)他のライブラリない強力な利点だと思います!
▼公式日本語リファレンス▼
サンプルレポジトリ
とにかく動かしたいという方は以下のレポジトリをCloneやDLしてみてください。
Assets/sample シーンを開き、Container
の UseRuby のチェックを外して 実行してください。
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
です。
-- モジュールテーブルを作成
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として定義しています。
local helpers = require ("Assets/Resources/Lua/commands.lua") --commandsの中身をhelpersに格納
local t = helpers.t --helpersのt関数を t と再定義
local o = helpers.o --helpersのo関数を o と再定義
このように定義することで、
次のような記法でテキストフィールドへの文字更新と、
ユーザーの文字送りボタン待機を実現しています。
t "今からサンプルADVを初めていくよ!"
DialogueManager.lua の内容
ADVを作るにあたって選択肢分岐はつきものです。
そこで、ユーザーの選択結果によって node を指定して jump できる記法が必要になります。
これを可能にするため、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
を直接記入できるようにしています。
-- 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
このように定義しておくことで、
次のように addNode
と jump
を使って会話スクリプトを記載することができます。
addNode
("first", function()
t "初めに実行されるノード"
jump "second"
end)
addNode
("second", function()
t "次に実行されるノード"
end)
-- ダイアログの開始
jump "first"
メソッドを順に実行するように書けば、通常の記法でもjump自体の実装は可能です。
ただし、Lua ではメソッド呼び出しより先にメソッド定義をしなければならないため、通常の記法では 分岐処理の後に分岐後の実装を書くことができません。
これでは取り回しが悪いため、DialogueManager を実装しました。
main.lua の内容
main.lua
の会話スクリプトは上記の組み合わせで出来ています。
選択肢の分岐処理に少し癖があるので追記しておきます。
上で定義した o メソッドを使うことで、
以下のように o の後に配列を渡すと選択肢が表示されます。
local result = o { "Yes", "No" }
実行時には以下のように右側に選択肢が現れます。
選択すると、戻り値である result にユーザーの選択結果が入ってきます。
以下のように result の内容を比較して、分岐処理を書くことができます!
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を実行するには、以下の手順を踏みます。
- LuaStateを生成し
- 必要なコマンドやモジュールを登録し
- ファイルを実行する
コードにすると以下の通りです。
//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
として登録できます。
書式は以下の通りです。
state.Environment["メソッド名"]=(new LuaFunction((context , buffer, token) =>
{
//メソッドの処理
}));
Lua内で "メソッド名" で登録した名前を呼ぶことでC#メソッドを実行できます。
サンプルプロジェクト上はLuaFunctionAdderでコマンドを登録しています。
ここからは、登録時の仕様について、
- Luaからの引数/返り値の利用
- syncメソッドとasyncメソッド
の順に解説します。
Luaからの引数/返り値の利用
Luaから与えられた引数は、Actionの引数になっているcontext
から取り出すことができます。
また、Luaに返り値を与える場合は、buffer
とLuaFunction
の戻り値を使います。
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メソッドになります。
以下のように利用できます。
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;
});
コマンドの登録については以上です。
コマンドの登録はより記述量の少ない [LuaObject]を利用する方法が公式で推奨されています。
今回は、C#側でInjectされたメンバ変数を利用したかったため、Environment経由の登録を使いました。
モジュールの登録
Lua-CSharpでは、Luaの標準ライブラリの一部を選択して読み込めるようになっています。
LuaStateへの拡張メソッドとして提供されているので、OpenLibsExtensions
を確認してみてください。
ここまでが、コマンドとモジュールの登録方法になります。
サンプルプロジェクトでは、LuaRunner内でLuaState.OpenModuleLibrary();
を行っています。
このライブラリを読み込むことで、Lua内で require
が宣言できるようになります。
C#とLuaで共通変数を扱う方法
C#とLuaで共通の変数を扱うことも可能です。
この場合もLuaState
を使います。
以下のようなコードでgetやsetが可能です。
//setする方法
LuaState.Environment["変数名"] = "任意の値";
//getする方法
LuaState.Environment["変数名"].Read<T>();
// LuaStateはLuaファイルを実行しているインスタンスを利用してください。
サンプルプロジェクトでは、LuaSharedStateHandlerで共通変数にアクセスしています。
その他のスクリプトの概観
サンプルプロジェクトはVitalRouterとVContainerを利用しています。
LuaFunctionAdderで登録したコマンドは、VitalRouter
のメッセージを発行しており、その完了待機をしています。
実際の処理は、TalkPresenterなどのPresenter側から辿ったほうが理解しやすいかと思いますので、必要に応じてご参照ください。
その他のPresenterも同じフォルダにまとめてあります。
メッセージをPublishした際、Presenter内の「Routeアトリビュートがついていて引数の型が一致するメソッド」が実行されます。
Unityの構成
Containerにほぼ全ての機能登録が集約しています。
選択肢のプレファブやテキストの表示登録はこちらにありますので、必要に応じて参照してください。
また、ContainerのUseRubyのチェックをつけると、Assets/Resources/Ruby/main.rb が実行されます。
こちらの詳細については別記事で解説しています。
おわりに
以上で Lua-CSharp を使ってアドベンチャー用のスクリプト環境を構築できました!
また、このサンプルはもともと Ruby と Lua の違いを確かめたく作ったものですので、
ContainerのUseRubyのチェックを外つけるだけで Assets/Resources/Ruby/main.rb が実行できます。
色々と試してみてご自身のプロジェクトに合う環境を探してみてください!