まとめ
- luabind を使って,期待していたことが実現できることを確認した
- 使い方のメモをまとめた
はじめに
luabind の公式
luabind の環境構築
当然だが,
困ったら公式ドキュメントを読むと良さげ.
※ 記事内で使用しているコードは,
実際にゲームを作っているときに書いているコードなので,
サンプル的な感じでわかりやすいものになっていない点は許して.
実現したいこと
luabind に期待していること,
実現したいことは以下.
- クラス情報の受け渡し
- 継承したクラス情報の受け渡し
- 変数,定数,関数の受け渡し
- C++ から Lua の関数を呼べること.
- Lua から,C++ の関数を呼べること.
- Lua から,C++ の変数にアクセスできること.
- Lua から,C++ のクラスやインスタンス,メンバにアクセスできること.
- 事前に Lua ファイルを読んでおき,オンメモリのバイト情報を渡して実行できること
- 異なる Lua ファイルの同名関数を呼び分けられること.
- Lua 内でエラーがあった場合にハンドリングできること
- 上記が比較的低コストで実現できること
概ね達成できたのでそのメモを残しておく.
初期化終了
luabind の lib をリンクし,
lua のヘッダと luabind のヘッダをインクルードする.
#include <lua.hpp>
#include <luabind\luabind.hpp>
初期化終了コードは以下.
// 初期化
lua_State* L;
L = luaL_newstate();
luaL_openlibs(L);
luabind::open(L);
// 終了
lua_close(L);
簡単.
変数,定数,関数の受け渡し
C++ ⇔ Lua の受け渡しには,
事前に C++ 側で登録が必要.
変数 ( enum や define を含む ) や,
グローバル関数の登録は以下のようにする.
// グローバル関数
luabind::module( LuaManager::GetLuaState() )[
luabind::def( "Input_GetKeyState", &Input::GetKeyState ),
];
// 変数
luabind::object g = luabind::globals( LuaManager::GetLuaState() );
g[ "KEY_LSHIFT" ] = KEY_INPUT_LSHIFT;
g[ "KEY_RIGHT" ] = KEY_INPUT_RIGHT;
g[ "KEY_LEFT" ] = KEY_INPUT_LEFT;
g[ "KEY_UP"] = KEY_INPUT_UP;
g[ "KEY_DOWN"] = KEY_INPUT_DOWN;
Lua からは,以下のように呼び出せる.
if Input_GetKeyState( KEY_DOWN ) >= 1 then
end
クラスの受け渡し
クラスの受け渡しは
以下のようにする.
// Entity クラス
luabind::module( L )[
luabind::class_<Entity>( "Entity" )
.def( "GetTransform", &Entity::GetComponent<Transform> )
.def( "GetRigidBody2D", &Entity::GetComponent<RigidBody2D> )
.def( "Destroy", &Entity::Destroy )
.def( "GetTag", &Entity::GetTag )
];
Entity というクラスを登録している.
メンバ関数として,
GetComponent
Destroy(),GetTag()
を持つ.
テンプレートの呼び分けは,
別の関数として登録してやれば良い.
メンバ変数については↓.
継承したクラスの受け渡し
継承したクラスを登録する場合,
継承していることを明示的に登録する必要がある.
具体的には以下のようにする.
// Enemy クラス
luabind::module( LuaManager::GetLuaState() )[
luabind::class_<Enemy, Entity>( "Enemy" )
.def( luabind::constructor<>() )
.def( "SetPosition", &Enemy::SetPosition )
.def_readwrite( "m_speed", &Enemy::m_speed )
.def_readwrite( "m_life", &Enemy::m_life )
];
Entity クラスを継承した,
Enemy クラスを登録している.
メンバ関数として
SetPosition(),
メンバ変数として
m_speed , m_life を持つ.
上記のように登録したい場合,
メンバ変数を public に持つ必要があるので注意.
嫌な場合は setter , getter を用意しよう.
クラスには lua からはこんな風に書ける.
// ap は c++ から渡ってきた Enemy のインスタンス
ap.m_speed = 2.0;
ap:Destroy();
メンバ変数の場合は「.」
メンバ関数の場合は「:」
な点に注意. *1
Lua 関数の呼び出し
以下のようにする.
luabind::call_function<void>(L, "Init", this);
戻り値 void の Init 関数に, this を渡して呼び出している.
lua 側は以下のような感じ.
function Init(ap) ap.m_speed = -1.8; ap.m_life = 4; end
事前に Lua ファイルを読み,オンメモリのバイトを渡して呼び出す
スクリプトを実行するたびに
ファイルを読みに行くわけには行かないので,
事前に lua ファイルを読んでおき,
オンメモリの状態で使用したい.
当然 lua にはその機能が用意されていて,
以下のようにする.
luaL_loadbuffer(L, buf, size, name); lua_pcall(L, 0, 0, 0);
事前に読んでおいた バッファと,
そのサイズと,デバッグ情報用*2の名前
をわたして luaL_loadbuffer .
一度 lua_pcallしておくことで,
オンメモリの lua を呼び出せる.
異なる Lua ファイルの同名関数を呼び分ける
満たしたいこととして,
異なる lua スクリプトの同名関数を呼び分けたい.
というのがある.
Unity の C# スクリプトのように使いたいので,
- Init()
- Update()
というシンプルな名前の関数のみを使いたい.
実現には,
ファイルを分けてやり,
上記の オンメモリの呼び出しをもう一度行えば良い.*3
つまり,
// A の Init が呼ばれる
int ret = luaL_loadbuffer(L, a_buf, a_size, "A");
lua_pcall(L, 0, 0, 0);
luabind::call_function<void>(L, "Init", this);
...
// B の Init が呼ばれる
int ret = luaL_loadbuffer(L, b_buf, b_size, "B");
lua_pcall(L, 0, 0, 0);
luabind::call_function<void>(L, "Init", this);
こんな感じ.
エラーハンドリング
別記事参照
実現できなかったこと
RTTI の無効化
RTTI をしたところ, ハングした.
どうやら RTTI は無効化できない.
昔はできたようだが,
0.9.1 ではその機能が削除されている.
*4
luabind を使用するには RTTI が必要.
おわりに
↓ 実際にゲームに実際に組み込んでみた.
よし!
luabind を使ってゲームに Lua を組み込み,ゲームオブジェクトを Lua で制御,実行中にスクリプトをホットリロードすることに成功!
これで爆速でトライアンドエラーができる!(はず↓ 例.実行中にスクリプトを書き換え,自機の弾の数を増やしている. pic.twitter.com/VWzHHNbZeL
— GOTH (@GOTH_bikelife) July 25, 2021
必要な全ての変数,関数,クラスの登録を行って Lua でスクリプト書くのと,
全て C++ でオブジェクトの挙動を書くの,
どちらが効率が良いだろうか.
所感としては
Lua を組み込んだほうが良さそうではあるが,
デバッガが欲しくなったりしてきたらどうなるか.
ていうかデバッガって使えるんだろうか.
調べて使えたらまとめてみる.