Developer

さくさく理解する Godot 入門(ただし2Dに限る)応用編 レトロシューティングゲーム【第2回】
2021.12.09
Lv1

さくさく理解する Godot 入門(ただし2Dに限る)応用編 レトロシューティングゲーム【第2回】

目次

  • スクリプト
  • 処理の全体像
  • 自機左右移動
  • 自機ミサイル発射
  • 自機ミサイル飛翔・衝突

スクリプト

■処理の全体像

パズルゲームなどのリアルタイム性が無いものは、マウスクリック・キー操作などのイベントが発生した時に、 それに対する処理を記述すればよい。
それに対してアクションゲームはマウス・キー操作がなくとも敵機等をリアルタイムに動かす必要がある。 なので、Godot では各フレームの最初に呼ばれる _process(delta) または _physics_process(delta) 関数を実装することでそれを実現する。 本アプリの場合は、物理ボディを使用しているので、後者の _physics_process(delta) 関数を実装する。

キー押下時のイベントハンドラは実装せず、_physics_process(delta) 内で操作キーが押されているかどうかを判定する(具体的には次節参照)。

■自機左右移動

自機の移動処理は、_physics_process(delta) で行う。 その中で、左右カーソルキーが押されているかどうかを判定し、自機を移動する。

const SCREEN_WIDTH = 500        # スクリーン幅
const UFO_MOVE_UNIT = 150       # UFO 飛翔速度
const MOVE_UNIT = 200           # 自機左右移動速度
const FIGHTER_LR_SPC = 64       # 自機左右端空白
const MIN_FIGHTER_X = FIGHTER_LR_SPC    # 自機左端位置
const MAX_FIGHTER_X = SCREEN_WIDTH - FIGHTER_LR_SPC     # 自機右端位置

func _physics_process(delta):
    .....
    var dx = int(Input.is_action_pressed("ui_right")) - int(Input.is_action_pressed("ui_left"))
    if dx != 0:
        $Fighter.position.x += dx * MOVE_UNIT * delta   # 左右移動
        $Fighter.position.x = max($Fighter.position.x, MIN_FIGHTER_X)   # 左端処理
        $Fighter.position.x = min($Fighter.position.x, MAX_FIGHTER_X)   # 右端処理

「var dx = 式」の部分は若干トリッキーな書き方だが、Godot の標準的な書き方のようなので採用している。 もちろん if 文で Input.is_action_pressed(“ui_right”), Input.is_action_pressed(“ui_left”) をチェックし、 dx を設定しても構わない。

int() の部分は、指定されたキーが押されていれば 1、押されていなければ 0 を返すので、 dx の値は、左矢印が押されていれば -1、右矢印キーが押されていれば +1 となる。

自機の位置は $Fighter.position が保持するので、左右キーのいずれかが押されていた場合は、更新する。 position.x に dx * MOVE_UNIT * delta を足し込む。MOVE_UNIT は移動単位、delta は以前に _physics_process() が呼ばれてからの経過時間(単位は秒)だ。
最後に自機が画面からはみ出ないように、座標チェック・更新を行っている。

■自機ミサイル発射

自機のミサイルは _physics_process(delta) の中で、ミサイル発射キーが押されているかどうかをチェックし、 押されていれば fireMissile() を呼ぶことで発射する。

func _physics_process(delta):
    .....
    if missile != null:     # 自機ミサイル飛翔中
        processMissile()    # 自機ミサイル飛翔&衝突判定処理
    else:
        if (Input.is_action_pressed("ui_select") ||     # Space Key
            autoMoving && abs($Fighter.position.x - autoMoveX) <= 2):
                autoMoving = false
                fireMissile()       # 自機ミサイル発射

fireMissile() は以下のように実装される。

var Missile = load("res://Missile.tscn")    # 自機ミサイル用(外部)シーンをロード
....
func fireMissile():     # 自機ミサイル発射
    if missile == null:     # 自機ミサイル飛翔中の場合
        UFOPntIX += 1       # UFO 得点のためのカウンタを+1
        if UFOPntIX == UFO_POINTS.size():
            UFOPntIX = 0
        missile = Missile.instance()            # ミサイルインスタンス生成
        missile.position = $Fighter.position    # 位置は自機の位置
        add_child(missile)                      # ノードツリーに追加
        $AudioMissile.play()        # 効果音再生

本アプリでは、自機ミサイルが飛翔中の場合は自機ミサイルを発射できないという仕様にしたので、 最初に自機ミサイルが飛翔中かどうかをチェックしている。ミサイルが飛翔中の場合は missile に飛翔中ミサイルへの参照を保持する。 飛翔中でない場合の値は null だ。

また、UFO 撃墜時の得点は何発ミサイルを発射したかで決まる疑似乱数なので、ここでカウンタをインクリメントしている。

ミサイルを生成するときは、ロードしたクラスから instance() でオブジェクトをインスタンス化し、座標を自機と同じにし、 add_child() でノードツリーに追加する。

■自機ミサイル飛翔・衝突

processMissile() は、自機ミサイルの飛翔と衝突判定処理を行う関数で、下記のように実装されている。

# UFO 撃墜時得点テーブル
const UFO_POINTS = [10, 10, 50, 10, 10, 50, 10, 10, 300, 10, 10, 50, 10, 10]
var mv = Vector2(0, MISSILE_DY)     # 自機ミサイル移動量
.....
func processMissile():              # 自機ミサイル飛翔&衝突判定処理
    if missile != null:             # 自機ミサイル飛翔中
        if missile.position.y < 0:  # 画面上部に出た場合
            missile.queue_free()    # オブジェクトを解放
            missile = null          # 飛翔中ミサイル消去
        else:
            # ミサイル移動 & 衝突判定
            var bc = missile.move_and_collide(mv)   # 移動&衝突判定
            if bc != null:      # 敵機に当たった場合
                missile.queue_free()    # オブジェクトを解放
                missile = null          # 飛翔中ミサイル消去
                if bc.collider == $UFO:         # UFO に当たった場合
                    $UFOLabel.rect_position.x = $UFO.position.x
                    $UFOLabel.text = "%d" % UFO_POINTS[UFOPntIX]
                    $UFOLabel/Timer.start()
                    $UFO.position.x = -1
                    score += UFO_POINTS[UFOPntIX]
                else:
                    remove_enemy(bc.collider)   # 撃墜した敵機を削除, score更新
                    bc.collider.queue_free()    # 撃墜した敵機のメモリを解放
                $AudioMissile.stop()        # ミサイル発射音停止
                $AudioExplosion.play()      # 爆発音
                updateScoreLabel()          # スコアラベル更新
                if nEnemies == 0:       # 敵をすべて撃破した場合
                    paused = true           # ポーズフラグをON
                    $NextLevel.show()       # ポーズ用レイヤー表示

飛翔中の自機ミサイルがある場合は、その参照が missile に保持されているので、最初に null でないかどうかをチェックしている。

ミサイルの y 座標をチェックし、画面上部に出た場合は、もうそのオブジェクトは必要ないので queue_free() をコールし、 メモリを解放し、missile を null に設定する。
queue_free() はそのフレームの処理が終わり画面表示が行われた後に安全にメモリを解放する関数だ。

ミサイルが画面内にある場合は、move_and_collide(mv) をコールしてミサイルを移動させる。 この関数は移動させるだけでなく、何かに衝突した場合は、その情報を返してくる便利な関数だ。

帰ってきた値が null でなければ衝突が発生したので、画面上部に出たとき同様にミサイルを消去する。

何と衝突したかは collider プロパティで取得できる。なので、最初に $UFO ノードと比較し、 一致していれば、UFO 位置を画面外に出すことで消去し、疑似乱数得点テーブルをひいて、得点計算を行っている。

TechProjin Godot入門 関連連載リンク

Godotで学ぶゲーム制作
さくさく理解するGodot入門 連載目次

標準C++ライブラリの活用でコーディング力UP!
「競技プログラミング風」標準C++ライブラリ 連載目次