弾幕ゲー開発用スクリプト言語を自作した話
なんとなくスクリプト言語を作ってみたくなったので、東方のような弾幕ゲーで敵の行動,弾幕パターンを記述するためのスクリプト言語を作ってみました。
(最近のゲーム業界ではわざわざDSLを作るより、LuaやHaxeのような汎用的なスクリプト言語と組み合わせるのが普通みたいですが)
近年コンパイラ基盤の普及で、比較的コンパイラ開発の敷居は下がってると思いますが、せっかくなので今回はLLVMなどのコンパイラ基盤を使わず、フロントエンドから
バックエンド,インタプリタまで全て自力でフルスクラッチ開発してみました。
またパーサージェネレーターなども一切使っていません。
フロントエンド
文法はLL(k)のような固定長ではなく任意長先読みで、パックラット構文解析器を実装しました。例えば敵の弾幕スクリプトは以下のように書きます。
let count=0; let cx=GetCenterX(); let imgEnemy = "img/Enemy.png"; let imgAngle=0; let shotAngle=0; function @Initialize() { LoadGraphic(imgEnemy); SetLife(1500); SetGraphicRect(1,1,32,32); SetTexture(imgEnemy); SetMovePosition02(cx+0,60,60); return; } function @MainLoop() { if (count >= 30) { if (count % 180 == 0 && count < 4000) { SetMovePosition02(cx-50,100,20); } if(count % 180 == 90 && count < 4000){ SetMovePosition02(cx+50,100,20); } if(count % 10 == 0){ let angle = 0; while(angle < 360) { CreateShot01(GetX() + GetW() / 2, GetY() + GetH() / 2,10,angle,0,1); angle += 20; } } } ++count; return; } function @Finalize() { } function @DrawLoop() { DrawGraphic(GetX(), GetY()); return; }
これで抽象構文木(AST)に変換します。
AST
オーソドックスなASTです。上のコードを変換してリスト形式っぽく吐くとこんな感じになります。
(DECL count 0) (DECL cx (CALL GetCenterX)) (DECL imgEnemy "script/img/Enemy.png") (DECL imgAngle 0) (DECL shotAngle 0) (FUNC_DEF @Initialize PARAM (BLOCK (CALL LoadGraphic imgEnemy) (CALL SetLife 1500) (CALL SetGraphicRect 1 1 32 32) (CALL SetMovePosition02 (+ cx 0) 60 60) RETURN)) (FUNC_DEF @MainLoop PARAM (BLOCK (IF (>= count 30) (BLOCK (IF (&& (== (% count 720) 0) (< count 4000)) (BLOCK (CALL SetMovePosition02 (- cx 50) 100 20))) (IF (&& (== (% count 720) 360) (< count 4000)) (BLOCK (CALL SetMovePosition02 (+ cx 50) 100 20))) (IF (== (% count 10) 0) (BLOCK (DECL angle 0) (WHILE (< angle 360) (BLOCK (CALL CreateShot01 (+ (CALL GetX) (/ (CALL GetW) 2)) (+ (CALL GetY) (/ (CALL GetH) 2)) 10 angle 0 1) (ASSIGN angle += 20))))))) (++ count) RETURN)) (FUNC_DEF @Finalize PARAM BLOCK) (FUNC_DEF @DrawLoop PARAM (BLOCK (CALL DrawGraphic (CALL GetX) (CALL GetY)) RETURN))
ASTを生成した後は、意味解析を行い型推論とシンボル定義を行います。
バックエンド
上のASTから3番地コードを吐かせます。
_Init: r1 = 0 count = r1 call GetCenterX 0 r2 = r0 cx = r2 r3 = "script/img/Enemy.png" imgEnemy = r3 r4 = 0 imgAngle = r4 r5 = 0 shotAngle = r5 @Initialize: param imgEnemy call LoadGraphic 1 r6 = r0 r7 = 1500 param r7 call SetLife 1 r8 = r0 r9 = 1 r10 = 1 r11 = 32 r12 = 32 param r9 param r10 param r11 param r12 call SetGraphicRect 4 r13 = r0 r14 = 0 r15 = cx + r14 r16 = 60 r17 = 60 param r15 param r16 param r17 call SetMovePosition02 3 r18 = r0 ret @MainLoop: r19 = 30 if count >= r19 goto label0 goto label1 label0: r20 = 720 r21 = count % r20 r22 = 0 if r21 == r22 goto label2 goto label3 label4: r23 = 4000 if count < r23 goto label2 goto label3 label2: (省略) call GetY 0 r48 = r0 call GetH 0 r49 = r0 r50 = 2 r51 = r49 / r50 r52 = r48 + r51 r53 = 10 r54 = 0 r55 = 1 param r47 param r52 param r53 param angle param r54 param r55 call CreateShot01 6 r56 = r0 r57 = 20 angle = angle + r57 goto label12 label14: goto label15 label11: label15: goto label16 label1: label16: count = count + 1 r58 = count ret @Finalize: @DrawLoop: call GetX 0 r59 = r0 call GetY 0 r60 = r0 param r59 param r60 call DrawGraphic 2 r61 = r0 ret
ゴミみたいに汚いコードが生成されていますが、これは最適化を一切実装していないからです。
今回は最適化までは実装しませんでした... しかしここまで汚くなる物なんだなぁ
上で生成した3番地コードをそのまま解釈できるインタプリタを作成し、ゲーム本体のAPIを逐一呼び出す事でゲーム画面に結果を反映する設計になっています。
実際に実行させるとこんな感じです。
静止画なので分かりづらいですが、敵が弾幕を撃ったりちょこちょこ動いたりしてます。
ちなみにゲーム本体の方は、途中で実装飽きて当たり判定もないしゲームとして成立していません。
ただのビューアになっている....
所感
言語処理系は初めて作ったのでコードもハチャメチャになってしまった。
あと反省としては意味解析をかなりサボっているのと最適化を実装していないため生成コードが酷い。
次からはLLVMを使おう....
参考文献
A.V. エイホ 他『コンパイラ―原理・技法・ツール』(サイエンス社 2009)
Andrew W. Appel『最新コンパイラ構成技法』(翔泳社 2009)
Terence Parrl『言語実装パターン ―コンパイラ技術によるテキスト処理から言語実装まで』(オライリー 2011)