目次
- UFO移動
- 敵機ミサイル
- 自機爆発
- まとめ
- おわりに
■UFO移動
UFO の移動は、各フレーム最初にコールされる _physics_process(delta) で処理される。
UFO が画面内にあれば、position.x に移動量を足し込むだけだ。
func _physics_process(delta):
.....
if $UFO.position.x > 0: # UFO 出現中
$UFO.position.x -= UFO_MOVE_UNIT*delta
.....
■敵機ミサイル
敵ミサイル発射処理は fireEnemyMissile() で、 EnemyMissileTimer のタイムアウト時にコールされる _on_EnemyMissileTimer_timeout() からコールされる。
func _on_EnemyMissileTimer_timeout():
fireEnemyMissile() # 敵ミサイル発射
fireEnemyMissile() の実装は下記の通り。
var EnemyMissile = load("res://EnemyMissile.tscn") # 敵機ミサイル用(外部)シーンをロード
.....
func fireEnemyMissile():
if gameOver || exploding || paused || !nEnemies:
return
var r = randi() % nEnemies # ミサイルを発射する敵
var ix = 0
while true:
while enemies[ix] == null: # 空要素はスキップ
ix += 1
if r == 0:
break
ix += 1
r -= 1
if enemies[ix] != null:
var em = EnemyMissile.instance()
em.position = enemies[ix].position
em.position.y += ENEMY_MISSILE_OFFSET
add_child(em)
while !enemyMissiles.empty() && enemyMissiles[0] == null:
enemyMissiles.pop_front() # 空要素削除
enemyMissiles.push_back(em)
最初に、ゲームオーバーや敵機がもういない場合などをチェックし、その場合はすぐにリターンする。
ミサイルを発射する敵をどれかひとつ乱数で決定する。randi() % nEnemies で [0, nEnemies) の乱数を発生させ、 while 文で、すでに消去された敵機 ix をスキップしながらループを回し、ミサイルを発射する敵機を決める。
敵ミサイルも外部シーンなので、instance() で実体化し、座標を設定し、ノードツリーに追加することで画面に表示する。
あとは、敵ミサイル管理配列の先頭が空であれば、その要素を削除し、今生成した敵ミサイルを管理配列の末尾に追加している。
厳密には、先頭だけを削除するのではなく、管理配列の途中も削除すべきかもしれないが、 配列要素の削除はメモリの移動を伴い、パフォーマンス的によろしくないので、このような方式にしている。 敵ミサイルは5発程度しか同時には生成されないので、あまり厳密に処理する必要はないと考えている。
敵ミサイルの移動・衝突判定処理は、_physics_process(delta) から呼ばれる processEnemyMissiles() で行う。
func _physics_process(delta):
.....
processEnemyMissiles() # 敵ミサイル処理
....
var emv = Vector2(0, ENEMY_MISSILE_DY)
.....
func processEnemyMissiles():
var ix = 0
for em in enemyMissiles: # 全飛翔中敵ミサイルの処理
if em != null:
var bc = em.move_and_collide(emv) # 移動&衝突判定
if bc != null && !exploding:
if bc.collider == $Fighter: # 自機に命中
explodeFighter() # 自機爆発処理
return
else: # トーチカに衝突
bc.collider.queue_free()
em.queue_free()
enemyMissiles[ix] = null
elif em.position.y >= 700: # 画面外に出た場合
em.queue_free()
enemyMissiles[ix] = null
ix += 1
敵機ミサイルは複数同時に飛翔し、それらは enemyMissiles で管理されている。なので for 文で回して順に処理している。 自機ミサイルの場合同様に、move_and_collide(emv) を使ってミサイルを移動し、衝突判定を行っている。
自機に衝突した場合は(次節で説明する)explodeFighter() が呼ばれ、パーティクルを使った爆発イフェクトが表示される。
■自機爆発
敵ミサイルが自機に命中した場合は、下記の explodeFighter() が呼ばれ、 爆発イフェクトのためのパーティクルが生成される。
また、自機の残機数を減らし、0になった場合はゲームオーバーとし、ダイアログを表示している。
func explodeFighter():
$Fighter/Sprite.hide() # 自機を非表示に
$Fighter/Explosion.restart() # パーティクルによる爆発イフェクト
exploding = true
clearAllMissiles(); # 敵ミサイル消去
dur_expl = 0.0
nFighter -= 1 # 残機数を減らす
if nFighter == 0: # 自機:0、ゲームオーバー
gameOver = true
$DlgLayer/GameOverDlg.window_title = "GodotShooting"
$DlgLayer/GameOverDlg.dialog_text = "GAME OVER\nTRY AGAIN ?"
$DlgLayer/GameOverDlg.popup_centered() # ダイアログを画面中央に表示
updateLeftFighter()
if missile != null: # 自機ミサイル飛翔中の場合
missile.queue_free() # 自機ミサイル消去
missile = null
■まとめ
・物理ボディを使用するリアルタイムゲームでは、_physics_process() で各フレームの処理を記述する。
・物理ボディノードを使用することで衝突判定処理を簡単に行うことが可能。
・move_and_collide() を使用することで物理ボディノードを移動して衝突判定を行うことが出来る。
・衝突判定を選択的に行うには、レイヤーとマスクを使用する。
おわりに
前シリーズの応用編でほとんど動きのない数独パズルをサンプルとして作成したので、 今回はそれとは打って変わって動きの多いアクション系シューティングゲームを実装・解説してみた。 Godot を使ってアクション系ゲームを作る方法の具体例を理解していただいたけたものと思っている。
余談だが、本アプリは何人もの方にテストプレイしていただいたのだが、予想以上に受けがよかった。 その最大の要因は、本アプリの元となった「スペースインベーダー」の仕様が非常に素晴らしかったためだと考えている。 現在のアクションゲームと比べると非常に単純な仕様にもかかわらず、意外と戦略性が高く、 多くの人が楽しめるものになっている。 C言語などの高級言語もまだ利用できずアセンブラしかない1970年代に、 このような素晴らしいゲームを設計・実装されたエンジニアの方々には尊敬の念をいだかざるをえない次第だ。
TechProjin Godot入門 関連連載リンク
Godotで学ぶゲーム制作
さくさく理解するGodot入門 連載目次
標準C++ライブラリの活用でコーディング力UP!
「競技プログラミング風」標準C++ライブラリ 連載目次