るくすの日記 ~ Out_Of_Range ~

主にプログラミング関係

弾幕ゲー開発用スクリプト言語を自作した話

なんとなくスクリプト言語を作ってみたくなったので、東方のような弾幕ゲーで敵の行動,弾幕パターンを記述するためのスクリプト言語を作ってみました。
(最近のゲーム業界ではわざわざDSLを作るより、LuaHaxeのような汎用的なスクリプト言語と組み合わせるのが普通みたいですが)

近年コンパイラ基盤の普及で、比較的コンパイラ開発の敷居は下がってると思いますが、せっかくなので今回はLLVMなどのコンパイラ基盤を使わず、フロントエンドから
バックエンド,インタプリタまで全て自力でフルスクラッチ開発してみました。
またパーサージェネレーターなども一切使っていません。

github.com


フロントエンド

文法は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を逐一呼び出す事でゲーム画面に結果を反映する設計になっています。

実際に実行させるとこんな感じです。

f:id:RKX1209:20160429180940p:plain

静止画なので分かりづらいですが、敵が弾幕を撃ったりちょこちょこ動いたりしてます。
ちなみにゲーム本体の方は、途中で実装飽きて当たり判定もないしゲームとして成立していません。
ただのビューアになっている....


所感
言語処理系は初めて作ったのでコードもハチャメチャになってしまった。
あと反省としては意味解析をかなりサボっているのと最適化を実装していないため生成コードが酷い。
次からはLLVMを使おう....


参考文献
A.V. エイホ 他『コンパイラ―原理・技法ツール』(サイエンス社 2009)
Andrew W. Appel『最新コンパイラ構成技法』(翔泳社 2009)
Terence Parrl『言語実装パターン ―コンパイラ技術によるテキスト処理から言語実装まで』(オライリー 2011)