ゲーム開発におけるスクリプト言語
ゲーム開発において、UIやゲームプレイなどを実装する際、スクリプト言語を使うことがよくあります。太古の事例でいうと、World of Warcraftのプラグイン開発ではLuaを使います。最近の日本の開発事例でいうと、ヘブンバーンズレッドとFFVII EVER CRISISはUnityで開発されていますが、ADVパートの開発はC#ではなく、Luaを採用しています。
Unityの場合、C#しかオフィシャルにサポートされてないため、Luaをサポートする場合は独自に実装する必要あります。UnrealEngineの場合、C++以外にBlueprintもオフィシャルにサポートされています。いずれにせよ、原理的にゲームエンジンがサポートしていないスクリプト言語を導入するには、スクリプト言語のインタプリタの組み込み、エンジン機能をアクセスするためのバインディングコードの生成が必要です。
スクリプト言語に厳密な定義はありませんが、通常はコンパイル不要のインタプリタ型言語がスクリプト言語とみなされます。これらの言語は通常、型がないか動的型付けであり、記述もその分簡単になります。
以上を踏まえて、エンジン言語(通常はC/C++/C#)を使用せずにスクリプト言語でゲームロジックを書く利点は明確です、
- スクリプト言語で書かれていた部分であれば、ゲームのバイナリを変更せずに(つまりバージョンアップとアプリストア再審査を必要としない)、修正後のコードをテキストアセットとして配布すれば、不具合のhotfixを行うことができます。
- 再コンパイルせずに変更を確認できるため(ホットリロード)、イテレーションを速めることができます。
- 非プログラマー(プランナー、演出など)が開発に参加するための敷居を下げられます。
一方で、ゲーム開発にスクリプト言語を導入するデメリットもあります、
- プロジェクトの複雑さが増し、エンジン言語とスクリプト言語両方のコードベースを管理する必要があります。
- インタプリタ型言語のパフォーマンスは通常、コンパイル型言語に劣り、パフォーマンスにシビアな場面に適していません。ゲームエンジン言語とスクリプト言語のランタイム間のやり取りが必要になるため、オーバヘッドが発生します(UnityにLuaを導入する場合、元々C++エンジンランタイムとC#ランタイムの間に一定のオーバヘッドがある上に、C#とLuaのランタイム間のオーバヘッドがさらに発生します)。
- コンパイルしない分、実行時に初めて分かるバグが多くなり、コード品質の保証とデバッグが難しくなります。
総合的に考えて、これらのデメリットを差し引いても、hotfixができるのとイテレーション高速化のメリットがやはり大きい。規模の大きな開発ほどスクリプト言語を導入する価値があると言えるでしょう。
Luaの欠点
ゲーム開発用のスクリプト言語を選択する際、高速・小さなランタイムサイズ・ゲームエンジンのランタイムと簡単にやり取りできる言語が望ましい。Luaはまさにこのような言語で、ランタイムサイズは数百KB程度で、実行速度が早く、Cに簡単に組み込めるようにデザインされてます。大規模ゲームプロジェクトでの採用実績もあり、多くのプロジェクトでスクリプト言語を導入する際の第一選択となっています。また、xLuaのような実績あるOSSがあり、UnityやUnrealのゲームにLuaの導入が容易。
しかし、2024年の視点から見ると、Luaにはいくつか大きな欠点があります。もっと良い選択肢がある今、大規模プロジェクトの開発にLuaの採用はあまりおすすめしません。
- 言語機能が貧弱、文法もモダン言語と大きな違いがあり、実は意外と勉強コストが高い
- 開発が活発ではありません
- Lua5.3からLua5.4になるまで5年かかりました、言語機能の改善があまり望めません。
- ベストプラクティスの欠如
- 今でもtableの内容の標準的な出力方法はありません。
- モジュールを定義する方法が少なくとも5つあり、クラスを定義する方法が3つあります。
- 標準ライブラリが貧弱、各プロジェクトが独自に車輪の再発明しているため、コードをプロジェクト間で再利用することが難しい。
ADVパートで、手続き的な記述しかしないみたいな限定的な使いかたでしたらLuaで全然いいですが、モジュールをつくり、もう少しOOP的なことをやろうとすると辛くなります。
TypescriptとPuerTSのススメ
そこでおすすめしたいのはES6以降のJavaScript/Typescriptです。 Luaと比べてTypescriptを使うことはいくつか明確なメリットがあります。
- Typescriptの静的型チェックの恩恵。
- IDEのサポートを受けられる。
- asyncのサポート。TypescriptからC#のasync関数をawaitできます。
- JavaScriptの豊富なエコシステムを利用できます。
- WebGLでのパーフォーマンス向上。
実際、xLuaの開発者もLuaの問題を認識しており、PuerTSを別途開発しました。Javascriptのランタイムは高性能なV8とコンパクトなQuickJSから選べます。
具体的な使い方はここで詳しく紹介しないですが、実際Typescriptでゲームロジックを書くのはどんな感じなのか、TypescriptからUnity UIにハンドラを登録する一例を紹介。
Unity Editor
ts側 (UIEvent.mts)
function init(monoBehaviour: CS.UnityEngine.MonoBehaviour): void { let button = monoBehaviour.GetComponent(puer.$typeof(CS.UnityEngine.UI.Button)) as CS.UnityEngine.UI.Button; let input = monoBehaviour.transform.parent.GetComponentInChildren(puer.$typeof(CS.UnityEngine.UI.InputField)) as CS.UnityEngine.UI.InputField; let toggle = monoBehaviour.transform.parent.GetComponentInChildren(puer.$typeof(CS.UnityEngine.UI.Toggle)) as CS.UnityEngine.UI.Toggle; button.onClick.AddListener(() =>{ console.log("button pressed..., input is: " + input.text); }); toggle.onValueChanged.AddListener((b) => { console.log("toggle.value=" + b); }); } export {init};
C#側 (UIEvent.cs)
using System; using UnityEngine; using Puerts; public class UIEvent : MonoBehaviour { // JavaScript runtime adaptor static JsEnv jsEnv; void Start() { if (jsEnv == null) { jsEnv = new JsEnv(); jsEnv.UsingAction<bool>();//toggle.onValueChanged } // ts側で定義したinitメソッドをActionにラップして、C#側で使えるようにする var init = jsEnv.ExecuteModule<Action<MonoBehaviour>>("UIEvent.mjs", "init"); if (init != null) init(this); } }
UIEvent.csはButtonにアタッチしているスクリプトで、JsEnvオブジェクトを通じてTypescript(正確にはコンパイル後のJavascript)で定義しているinit
メソッドを呼び出してことがわかります。
パフォーマンス比較
PureTSの公式サイトにPureTSとxLuaのベンチマークも乗っています。
長いので、まとめるとJITが使える環境であればPuerTSはxLuaに完勝、IOSみたいなJITが使えない環境であれば、PuerTSとxLuaの実行速度は同等です。
まとめ
言語機能、実行速度、エコシステム、IDEサポート、どれを取ってもTypeScriptが勝っている現在、 プロジェクトにスクリプト言語を導入するなら、敢えてLuaを採用する理由はないと思います。