※本記事にはアフィリエイトリンクが含まれています。
Amazonのアソシエイトとして、ゲームのタネは適格販売により収入を得ています。
▶ この回の完成形を、この場で遊ぶ
ブラウザ版と同じ核(道を進む敵の出現と、砲台の射程)を、Godot 4.7 向けに1本にまとめたミニゲームです。1波(10体)をすべて撃破すればクリア、1体でも基地に着くと失敗です。
記事中の GDScript・ダウンロード ZIP と同じルールです。はじめる(または画面クリック、R)で開始。プレイ中クリックで砲台を移動。
Godot の完成プロジェクト
上で遊んでいるものと同じゲームです。ZIP を解凍し、Godot 4.7(執筆時点の安定版は 4.7.2)で project.godot を開いて F5。
one_wave_defense.zip をダウンロード
はじめに
「敵は出た。砲台も置いた。でも、いつ撃てて、いつ撃てないの?」——そんな疑問向けです。
この記事では、Godot 4.7 で1波の防衛を1本の完成ゲームにします。敵は道のウェイポイントを進み、砲台は射程内の最も近い敵だけを自動で撃ちます。10体すべてを基地の手前で倒せばクリア、1体でも基地に着くと失敗です。R(またはボタン)で何度でもやり直せます。
今週身につけること:出現(スポーン)と射程(レンジ)。 Timer の timeout で敵シーンを instantiate して場に出します。砲台は Vector2.distance_to で距離を測り、射程以下のときだけ発射します。勝敗とやり直しは、それがゲームになるための枠です。
完成形と、やらないこと
- 作るもの: 固定の1本道。敵10体が間隔を空けて出現。砲台1種(自動射撃・位置はクリックで移動)。クリア/失敗とやり直し。Godot ZIP と、同じルールのイン記事プレイ。
- やらないこと: 複数ウェーブ、アップグレード、壁設置、タワー種類の切り替え、音、ランキング、Unity 併記。
- 持ち帰る1アイデア: 「出すタイミング」と「届く距離」を分けて持つ。出現は Timer、攻撃可否は距離チェック。


準備(Godot 4.7)
- Godot 公式ダウンロードから 4.7(安定版 4.7.2)を入れます。
- 上の one_wave_defense.zip を解凍し、
project.godotを開きます。記事のコードは仕組みを説明する抜粋です。初めて試すときは ZIP の完成プロジェクトを使ってください。 - メインシーンは
main.tscn(ルートはNode2D)です。
初心者がハマりやすい点: 盤面の塗りつぶしを CanvasLayer の全面 ColorRect に置くと、2D の敵や砲台が HUD の下に隠れて見えなくなります。第1回の的あてでも起きた失敗です。道とキャラは Node2D 側に置き、HUD は上部バーとラベルだけを CanvasLayer にします。

敵の出現(スポーン)
1波は全部で10体です。開始直後に1体出し、あとは Timer の wait_time(この完成版では 0.85 秒)ごとに1体ずつ出します。敵の見た目と動きは別シーン enemy.tscn にまとめ、PackedScene.instantiate() で複製します。公式ドキュメントの「Nodes and scene instances」でも、弾や敵を量産する定番手順として紹介されているやり方です。
スクリプト1:main.gd(Timer で出現)
以下は出現処理を説明する抜粋です。PATH の定義や開始処理などは含みません。完成版の main.gd は ZIP にあります。
extends Node2D
## Timer の timeout で敵シーンを1体ずつ出現させる(抜粋)。
@onready var spawn_timer: Timer = $SpawnTimer
@onready var enemies_root: Node2D = $Enemies
var enemy_scene: PackedScene
var spawned: int = 0
const WAVE_SIZE := 10
func _ready() -> void:
enemy_scene = preload("res://enemy.tscn")
spawn_timer.timeout.connect(_on_spawn_timeout)
func _start_wave() -> void:
spawned = 0
_spawn_one() # 最初の1体はすぐ
spawn_timer.start() # 以降は wait_time ごと
func _on_spawn_timeout() -> void:
if spawned >= WAVE_SIZE:
spawn_timer.stop()
return
_spawn_one()
func _spawn_one() -> void:
var inst: Node2D = enemy_scene.instantiate()
enemies_root.add_child(inst)
inst.setup(PATH) # ウェイポイント配列を渡す
spawned += 1
別ゲームに持ち替えるときの変更点: WAVE_SIZE と wait_time、enemy_scene の中身だけ変えれば、シューティングの弾幕やクイズの次の問題カードにも使えます。

砲台の射程(レンジ)
砲台は常に「一番近い敵」を狙いますが、距離が射程を超えていたら撃ちません。Godot 4.7 では Vector2.distance_to で2点間の距離を取れます。クールダウン中は撃てない、という時間側の制限も合わせて持たせます。
スクリプト2:turret.gd(射程判定)
ファイル名:turret.gd。以下は仕組みを示す抜粋です。完成ZIPには描画などを含む全文があります。
extends Node2D
## 射程内の最も近い敵へ自動攻撃する砲台。
@export var fire_range: float = 140.0
@export var fire_cooldown: float = 0.40
@export var damage: int = 1
var cooldown_left: float = 0.0
func _process(delta: float) -> void:
if cooldown_left > 0.0:
cooldown_left -= delta
func try_fire(enemies: Array) -> void:
if cooldown_left > 0.0:
return
var nearest: Node2D = null
var nearest_d := fire_range + 1.0
for e in enemies:
if e == null or not is_instance_valid(e):
continue
var d: float = global_position.distance_to(e.global_position)
if d <= fire_range and d < nearest_d:
nearest_d = d
nearest = e
if nearest == null:
return
nearest.take_damage(damage)
cooldown_left = fire_cooldown
別ゲームに持ち替えるときの変更点: fire_range を広げると「遠くまで届く弓」、狭くすると「近接だけ」。敵ではなくアイテム回収の「拾える距離」にも同じ距離チェックが使えます。
道を進む敵
敵は配列で持ったウェイポイントを順に move_toward します。完成ZIPでは速度とdeltaから1フレームの移動量を計算し、目標点への到達を判定します。最後の点(基地)に着いたら敗北シグナルを出します。HP が0になったら撃破です。
スクリプト3:enemy.gd(道の移動)
ファイル名:enemy.gd。以下は道の移動を示す抜粋です。完成ZIPには位置調整や描画などを含む全文があります。
extends Node2D
## ウェイポイントを進む敵。
signal reached_base
signal died
@export var speed: float = 70.0
@export var max_hp: int = 3
var hp: int = 3
var path: Array[Vector2] = []
var waypoint_i: int = 0
var alive: bool = true
func setup(waypoints: Array[Vector2]) -> void:
path = waypoints.duplicate()
hp = max_hp
waypoint_i = 0
if path.size() > 0:
global_position = path[0]
func take_damage(amount: int) -> void:
if not alive:
return
hp -= amount
if hp <= 0:
alive = false
died.emit()
queue_free()
func _process(delta: float) -> void:
if not alive or path.is_empty() or waypoint_i >= path.size():
return
var target: Vector2 = path[waypoint_i]
global_position = global_position.move_toward(target, speed * delta)
if global_position.distance_to(target) < 1.0:
waypoint_i += 1
if waypoint_i >= path.size():
alive = false
reached_base.emit()
queue_free()
別ゲームに持ち替えるときの変更点: ウェイポイントを増やす、速度を敵タイプごとに変える、max_hp を上げる、など。道そのもののデータと移動ロジックは分けたままにします。

エディタでの組み立て手順
- ZIP の
main.tscnを開きます。以下は完成シーンの構成を確認するための説明です。記事中の抜粋だけでは、経路データ、開始処理、勝敗判定、HUD と描画の実装が不足するため、空のプロジェクトからは完成しません。必要なスクリプトと設定は ZIP で確認してください。 - ルート
Main(Node2D)にmain.gdを付けます。 - 子に
BoardBg(Polygon2D・背景)、FieldDraw(Node2D +field_draw.gd)、Enemies(Node2D)、Turret(Node2D +turret.gd)、SpawnTimer(Timer)を置きます。ここがプレイフィールドです。 - 子に
UI(CanvasLayer)を作り、上部だけColorRect、Label、Button、結果用 Overlay を置きます。フィールド全体を覆う ColorRect は作らないでください。 enemy.tscn(Node2D +enemy.gd)を別シーンとして保存し、F5 で Play します。
Play で確認すること
- はじめるまで敵が出ない(自動開始しない)。
- 開始後、敵が道に沿って現れ、間隔を空けて続く。
- 砲台の薄い円(射程)の中にいる敵への射撃を線で表示する。
- 射程の外を通る位置に砲台を移すと、基地に着かれて失敗しやすい。
- 10体撃破でクリア、R で再開できる。
- (学習の確認)
fire_rangeを一時的に 60 に下げ、射程内の敵にだけ攻撃するか確認する。
できたこと
- Godot 4.7 で「1波の防衛」を1本完成させた。
- イン記事プレイ・記事の脚本・ZIP が同じルールになった。
身についたこと
TimerとPackedScene.instantiateによる出現。Vector2.distance_toによる射程判定。- プレイフィールドを CanvasLayer 全面で隠さない配置。
使い回しメモ
- スニペット:
main.gdの出現ループとturret.gdの距離チェック。 - アイデア: 「出す」と「届く」を別の仕組みにする。
- 次の用途: シューティングの弾の有効距離、クリックで拾えるアイテムの半径、会話が始まる距離。
関連商品(PR)
砲台を置いて距離を見る回なので、Godot の入門書と、クリック操作がしやすいマウスを挙げます。
- ゆるっとはじめる Godot Engine ゲームプログラミング(杉山悠真) Amazonで見る
※本リンクはアフィリエイトリンクです。 - Logicool G304 ワイヤレスゲーミングマウス(砲台の配置クリックとプレイテスト向け) Amazonで見る
※本リンクはアフィリエイトリンクです。
参考
- Godot Engine 公式ダウンロード(本記事執筆時の安定版 4.7.2)
- Download for Linux – Godot Engine(バージョン確認に使用)
- Timer — Godot 4.7 クラスリファレンス
- PackedScene — Godot 4.7 クラスリファレンス
- Vector2 — Godot 4.7 クラスリファレンス(distance_to)
- Node2D — Godot 4.7 クラスリファレンス
- Nodes and scene instances — Godot 4.7 ドキュメント
次に足してよい一言
砲台を2基にする、敵の速度を2種類にする、射程を画面に数字で出す——どれか1つで十分「自分のアレンジ」になります。
前回: 第4回 1戦闘のターン制(Unity)
次回予告(キュー): Unity で1部屋の3D(アイテムを取って出口へ)。カメラとレイを学びます。