3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-01-02

何の記事?

AnnulusGames さんが公開されているLua-CSharpというライブラリを使って、
アドベンチャーゲームを作るための基礎的な仕組みを制作してみました!
用意した機能は以下の四点です。

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

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

VitalRouter.MRubyを使って同等の機能を実装した記事も公開しています。
同じサンプルプロジェクトで両方確認できますので、ぜひ両方試してみてください。

Lua-CSharpとは

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

Lua実行ライブラリは MoonSharpxLuaなど、様々ありますが、Lua-CSharpは以下のような特徴があります!

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

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

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

サンプルレポジトリ

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

Assets/sample シーンを開き、Container の UseRuby のチェックを外して 実行してください。

container.png

Assets/Resources/Lua/main.lua の中身 が実行されます。

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

  • Luaの構成
  • Scriptの構成
  • Unityの構成

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

Luaの構成

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

commands.lua の内容

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

talkoptionは会話を書く上で頻出するため、省略記法を定義しています。
その定義ファイルがcommands.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上で、次のように定義して to をfunctionとして定義しています。

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 と再定義

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

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

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

DialogueManager.lua の内容

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

これを可能にするため、DialogueManagerを 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で次のように定義することでaddNodejumpを直接記入できるようにしています。

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

このように定義しておくことで、
次のように addNodejump を使って会話スクリプトを記載することができます。

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" }

実行時には以下のように右側に選択肢が現れます。

image.png

選択すると、戻り値である 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を実行するには、以下の手順を踏みます。

  1. LuaStateを生成し
  2. 必要なコマンドやモジュールを登録し
  3. ファイルを実行する

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

//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に返り値を与える場合は、bufferLuaFunctionの戻り値を使います。

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で共通変数にアクセスしています。

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

サンプルプロジェクトはVitalRouterVContainerを利用しています。

LuaFunctionAdderで登録したコマンドは、VitalRouter のメッセージを発行しており、その完了待機をしています。

実際の処理は、TalkPresenterなどのPresenter側から辿ったほうが理解しやすいかと思いますので、必要に応じてご参照ください。

その他のPresenterも同じフォルダにまとめてあります。

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

Unityの構成

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

また、ContainerのUseRubyのチェックをつけると、Assets/Resources/Ruby/main.rb が実行されます。
こちらの詳細については別記事で解説しています。

container.png

おわりに

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

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

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

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?