はじめに
この文章は、UnityAdventCalender2023の16日めです。
また、K3 AdventCalenderの25日目にも書いておきます。(ごめん、書く余裕ないorz)
概要
MachiaWorksと申します。
ゲーム・ツール・メガデモ・シンセサイザー・楽曲等色々作ってます。
今は主にUnityを使ってゲームやツールの開発中です。
今回は、Unityでも珍しい(あまり類似情報を見ない)C#上で組み込むスクリプト言語使ってますよ、ゲームをスクリプトで制御するの楽しいぜ!という話を書いていこうかと思います。
勿論組み込みのデメリットも存在するのでどうやって回避しているかという事例も織り交ぜて書いていきたいところです。
(過去にも類似文章書いてますのそちらも参照いただければ幸いです)
※なお、UnityではC#の事スクリプトと言ったりしてややこしいので、
この文章では可能な限り以下の形で記載していきます。
- C#で書くプログラム:プログラム(内部ロジックを記載するため)
- Miniscriptで書くプログラム:スクリプト
スクリプト言語・Miniscriptについて
Miniscript
この言語はC#で実装が書かれています。(今はC++でも実装されている)
元々Unityへの組み込みを想定されており、組み込みの方法がIntegrationGuideという形で記述がまとめられています。
そもそもマイナー言語扱いで日本語情報がない!という理由で、自分の方でWebページに情報をまとめてます。よかったら見てみて。
どんな言語なのか
クイックリファレンスも公開されています。
仕様は1ページに収まるくらいなんですよ。
文法についても意図的に簡単に書けるようにしてあるとのことで、プログラム入門にも最適な言語になっております。
最近のバージョンまで「+=」みたいな代入演算子の記述も意図的に入れてなかったみたいですからね!(Discordやブログ等で意見募って開発することにしたという)
また、最初から用意されている関数についてもそれほど不足はないです。(さすがにPythonみたいな潤沢さはないですが)
必要があれば自分で関数を作成することができ、各種Unity上で作った関数をスクリプトから呼び出す等も行いやすくなっております。
なお、言語の思想・開発した背景について、以下に開発者による文章があります。
下記にサンプルプログラムを記載します。
// for、if分岐について記述
for i in range(0,6)
if i==0 then
print i
else
print "none"
end if
end for
//リストを利用した文法
//foreachみたいなことができる。
lst=["one","two","three"]
for i in lst
print i
end for
//マップを利用した文法
mp={"one":"ichi", "two":"ni","three":"san"}
for kv in mp
print "key is " + kv.key +" and value is " + kv.value
end for
開発事例
Miniscriptの開発者がファンタジーコンソールのような位置づけで開発中の環境になります。
また、ソースコードはC#/C++両方で書かれており、C++版を利用したコマンドラインアプリも公開されております。
ブラウザ上で動作確認可能なページも公開されているため、簡単にお試しもできます。
あとは拙作で恐縮ですが、公開しているゲームの殆どはMiniscriptで制御してます。
https://booth.pm/ja/items/4741311(ゲーム)
https://machiaworx.itch.io/kyukoku-no-tori(ゲーム)
https://machiaworx.net/?p=2656(ゲーム)
https://machiaworx.itch.io/recode-livecoding(作曲用ツール)
下のスクショでは、「ステージ(敵やボス出現タイミング、シーン切替等)の制御」「プレイヤーの制御」「ボスの制御(2枚め)」にMiniscriptを使っております。
Miniscriptをゲーム・ツール開発に組み込むメリット・デメリット
メリット
内部処理の細かい制御が簡単に可能
まずMiniscriptは上記の通り四則演算や分岐、繰り返しや文字列処理等が行なえます。
例えば、何秒後(何フレ後)に処理したい!という動作制御も書けてしまうわけですね。
また、UnityのGameObjectへの変数(座標やHP等のパラメータ)のバインドも簡単に行えるため、オブジェクト制御についても簡単に準備可能です。
実際日本語Wikiに実装のフローを記載してみましたが、結構楽に制御ができています。
なお、変数についてはローカル制御になっており、staticな変数等のアクセスはバインドするクラス固有でのみできる模様。
修正に関する時間が軽減される
例えば、C#で書いたプログラムを後で修正する場合、コンパイルや再配置に時間がかかったりします。このため修正を反映するのに時間がかかってしまうケースが存在します。
C#のソースコードの量が増えて上記のような状況を経験した方は多いのではないでしょうか。
Miniscriptの場合、実行時に専用のアセンブリ言語に近い形式にコンパイルされるため、事前のコンパイル・再配置時間は発生しません。
また、コンパイルも一瞬で終わってしまうため、トータルでそれほど時間がかかりません。
よって、リトライの時間も軽減できます。
ただコンパイル時に内容の最適化は行われないため、純粋にアルゴリズムの選定・実装を行っていく必要があります。
テキストエディタだけで調整ができる
専用GUIではなく使い慣れたテキストエディタを使って記述ができます。
VSCodeのExtensionもあり、自分はこれを導入+改造してます。
デメリット
動作が遅い
当然ながらC#のプログラムをILにコンパイルして動作させるのと比較すると速度に差がでます。
また、IL2CPPでネイティブ化したら更に速度に差が開きます。
よって、速度が求められる部分を記載する場合は素直にC#のプログラムを記載する方が早いものと考えます。(もしくはdllを作成する等。この場合C++/CLIも選択肢に入ります)
自作ゲームの実装では、「スクリプトで動きを指定するオブジェクト」「プログラムで動きを指定するオブジェクト」を分けて管理し、前者の管理を少なくする事で高速に動くオブジェクトを増やす等しています。
また、一部オブジェクトの動き制御に対しJobSystemを採用することで全体的な処理速度の底上げを行っています。
自作のツールでは、波形を計算する必要があったため、波形出力部分はC#で書いています。
NativeAudioPluginで作るのもひとつかと思いましたが、一度後回しにしてます。
利用するとGCが発生する
スクリプトは文字列で持つ必要があり、stringで確保してから実行時にコンパイルを行いますが、Miniscriptにおいてコンパイル後の情報は、内部で仮想的なアセンブリ言語で保持しており、これもstring型で持っているようです。
(Discordで開発者とのやりとりがあり、内部処理の変更の可能性が高いため、あえて別途のコンパイル機能をもたせてないみたいです)
すなわち、現状の実装ではスクリプトのコンパイル時および実行時にstring型の変数にアクセスする形になり、GCが発生します。
また、前述の通りUnityのオブジェクトと連携するのに関数を作成することができますが、関数をスクリプト側で実行する際はdelegateを利用しており、実装方法によってはGCが発生するケースがある模様。
参考:
自作ゲームでは、Unityの設定でインクリメンタルGCを設定して相殺しています。
また、クラス上に値を一時的に格納するためのキャッシュ領域を設けてこれを利用することで少しGCを回避できてるかな?という状態です。
コントロールの仕組みを作るのに時間がかかる
当然ながらオブジェクトが持つ座標やパラメータ等をスクリプト上で読んだり計算できるように関連付けしていく必要があるわけですが、これを1から作る必要があるわけで、いきなりオブジェクトを動かすにも時間がかかってしまいます。
ただ、マニュアルに詳細な記載があるため、それほど困らないものと考えます。
(上記IntegrationGuide.pdf参照)
また、同じく上記の日本語ページを見てもらうと分かるとおり、それほど導入に時間はかからないはずなので、実際に導入いただくのが確認するのに一番早い方法かと思います。
他の選択肢はなかったのか
当然ながら突然スクリプトを書くと決めたわけではなく、色々と選択や選定をしていたわけですが、ちょっと私見を加えた上で判断を書いていこうかと。
ScriptableObjectで動き指定
一時期、ScriptableObjectにデータを書いて、それらをCSVデータを読み込む要領で、カラム目に命令の番号、2カラム目・3カラム目に変数、という形でのプログラミング言語(DSL)を作成して、オブジェクトを制御していました。
ただ、UnityEditorが頻繁に強制終了するようになり、その際に入力したデータ内容がすべてクリアされてしまう状況が頻発するようになりました。
また、追加で制御が必要になる場合、命令が1個追加されるようになります。
これを繰り返していくと、命令が爆発的に増えてEditor上で命令を入力していくのが困難になります。(なりました)
更に言うと、特定のタイミングでの起動(分岐や繰り返し等)もこの形式で書くのには無理があり、無理やり実装しようとすると複数のScriptableObjectを利用しジャンプして処理する、等の構造にせざるを得ませんでした。
以上の理由から、当該方法はあえなく取りやめになり、「Editor以外のところで保存する」「制御構文を持つ」「Unityに組み込み可能な言語」を色々探すことになりました。
ノードエディタ
ノードエディタはShaderGraphとか便利ですよね。周期だったり事前に色等の情報を定義したり。
ただ、オブジェクト制御を行うのにノードエディタだと定義しづらいところがあり、最初から取りやめになりました。
具体的には「特定のタイミングのみキックする」「条件分岐」を細かく制御するのを想定しており、これはプログラミング言語じゃないと制御がままならないなという判断に至りました。
あと単純に開発にも時間がかかると思うんですよねこの形式は・・・
CSVファイル
CSVファイルは読み込みが簡単なので敵の配置等で利用しており、最初はこれを使おうと思いましたが、ScriptableObjectと同様に命令の爆発的な増加問題が発生するため取りやめにしました。
その他のEditor拡張
某STGみたいにUnityEditor上で弾を吐く数を制御する等を指定、という方法もあるかと思います。
参考:
ScriptableObjectと同様にUnityのInspector上で制御するのは限度がありました。
また、ScriptableObjectと同様に命令の爆発的増加問題が存在します。
Unityで速度を求めるとしたらこちらの方法に収束するかと思うのですが、自作ゲームではスクリプトとオブジェクト周りの実装を組み合わせた結果十分な速度がでてしまったため、採用せず。
まとめ
- Unityにスクリプト言語を組み込んで動かす選択肢がある
- メリットが大きく、デメリットも場合によっては回避可能
- 文法や組み込みも簡単なので導入しやすい
ということで皆さんもMiniscript使ってみてはいかがでしょうかというご紹介になります。
おまけ
論文
他言語との処理速度比較
自作ゲームでの記述サンプル
//各種組み込み関数で取得してくる変数をスクリプト側にキャッシュする。
//関数呼び出しを計算ごとに行うよりは処理負荷とGCで確保するメモリの量を抑えられる。
frm= en_get_frame()
hp=en_get_hp()
//自作の組み込み関数で動きを指定。sin/cosは最初から用意されている組み込み関数。
en_move( 0.02*sin(frm*0.1+1.732), 0.005*cos(frm*0.1+1.732) )
//if分岐
//経過フレーム数ごとに定期的に処理を起動する。
if floor(frm %120) ==0 then
//自作の組み込み関数でエフェクト出力を行う
en_fx_entry( 0, 0.0,0,0,5, 0)
en_fx_entry( 0, 0,0,0,7, 0)
//rndはランダム数値を返す組み込み関数。
deg_rn=rnd*19-10
en_fx_entry( deg_rn, 0.25, 0,0,6, 0)
deg_rn=rnd*19-10
en_fx_entry( 120+deg_rn, 0.25, 0.0,0.0,6, 0)
deg_rn=rnd*19-10
en_fx_entry( -120+deg_rn, 0.25, 0.0,0.0,6, 0)
//サウンドマネージャを自作し、組み込み関数を起動して発音キューを飛ばせるようにした。
en_sound_on(11)
end if