最初に(興味なかったら飛ばしてね!)
自作でゲームエンジンを作るにあたって(自分は結局作らなかったけど)スクリプティングシステムはなくてはならない機能です。
実装手法には様々あると思います。
聞いたことがある範囲だとC#呼び出しとかLuaなどのスクリプト言語とか
自分は実際にLua言語を使ってスクリプティングをしようと考えていたのですが、Luaのデータ受け渡しの設計があまり肌に合わなかったのでC++で実装してみることにしました。
それっぽいことを書いたものの、実際にはUE4でリロードされてるのを見て技術的な興味が湧いたことが実装理由の大部分を占めています
ここから技術の話
まず簡単に実装手順について書いていこうと思います。
プロジェクトを2つ用意します(1つはDllを呼び出す側、もう1つはDllにする側)
1:プログラム(プロジェクト)を用意して
2:何らかのBuildツールを使ってDllに吐き出します。
3:後はリンクしたらできた!
実際はこんなに簡単ではないのでもう少し詳しく書きます。
特にDll内の設計は個人の設計によるとしか言えない部分も多いので自分の実装での話にしようと思います。
自分のプロジェクトの設計を簡単に図面で紹介します。
エンジン側をExeとして起動して、Dll化したゲーム側の更新関数と描画関数を呼び出すことでゲームとして動かしています。
この設計にすることで、実行中にエンジン側からcmakeを使ってゲーム側のDllをビルドすることができるようになります。
ここから自分の実装例を疑似コードで紹介します。
ホットリロードをする手順のコードです。
// 実体を破棄する前に書き出す(後述)
mpSceneSystem->ExportReload();
// 現在のシーンを終了する
mpSceneSystem->Finalize();
// 既存の実体を破棄
if(mpSceneSystem)
{
delete mpSceneSystem;
mpSceneSystem = nullptr;
}
// Dllをすでに読み込んでいた場合破棄する
if(mModule!=nullptr)
{
FreeLibrary(mModule);
}
// cmakeを使ってDllをビルド
BuildDll();
// ビルド後のDllをロード
mModule = LoadLibraryA("Game.dll");
if (mModule != nullptr)
{
// エラー処理(環境とか自分のやり方に合わせてください)
}
// 実体を生成
const auto creator = reinterpret_cast<DllLoader>(GetProcAddress(mModule, "CreateSceneSystem"));
mpSceneSystem = creator();
最後の // 実体を生成 とコメントしているところは、Dllから公開されている「シーンの実体を作成してそのポインタを返す関数」を呼び出すコードです。
ゲームループ中にこの処理を走らせて、mpSceneSystemを新しいものに更新してしまえば、リロード完了です。
ゲームループではmpSceneSystemを使って更新関数や描画関数を呼び出しています。
いろんなところにエラー処理が必要だと思うのでそのあたりは環境や自分のやり方に変えてください。
また、コメントの内容でそれぞれの関数が実装されているので足りない実装はそれぞれでお願いします。
m~はメンバ変数p~はポインタです。
ここまででソースコード自体のホットリロードはできるようになりました。
(まだcmakeを使ったビルドの方法とか依存関係の構築とか課題はあるけど...)
実際の開発で起こった障害とその対処法
ここからは実際の開発で起きた障害とその対策を紹介します。
まず、1番大きな障害はリロード前のデータが失われるということです。
これはDllを破棄してしまうとその後はmpSceneSystemに入っているポインタにアクセスできなくなるのでゲーム内のコードにアクセスできなくなることによっておきます。
この障害は、ゲーム内のデータをすべて、Dllを破棄する前にセーブして、Dllをビルド後ロードすることによって解決しました。
理論だけなら簡単ですが実際には設計なども絡んでくるので複雑になってしまうと思います。
自分の具体的な解決策を紹介します。
自分は「cereal」というライブラリを使用しました。
このライブラリはQiitaで他の人も紹介してくれている通り、とても使いやすく、リファレンスも豊富なので導入しやすいと思います。
特に、ポインタや継承関係もしっかり保持したままデシリアライズ・シリアライズすることが可能なのでゲームのような複雑で大きなデータを書き出すのには重宝しました。
URL
https://github.com/USCiLab/cereal
http://uscilab.github.io/cereal/index.html
2つ目はデバッグシンボルが壊れてしまうことです。
デバッグシンボルとはデバッガーを動かすのに必要なファイルで、リロード前から1文字でも変更されてしまうとブレークポイントなどが使えなくなってしまいます。
自分はリロード後にデバッグシンボルを新たに生成し、リンクすることで、解決しました。その際名前が被らないように日付_時間で命名規則を設定することで対応しました。
実装した感想
当初の目的であったリロードは完全に達成することができたので良かったと思います。
また、実装にあたってcmakeやDllなど普段の開発で使っていない分野の技術を勉強できたのは楽しかったです。
しかしスクリプト言語と違い、リロードにコンパイルという手間が必要で、その速度がイテレーションに直結するので、設計や、コードを書く技術力が使用者にも必要だと感じました。
また、シリアライズ時の保守にもコストがかかってしまうことも分かったので、保守をより簡単にする技術についても学んでいきたいです。