※本記事にはアフィリエイトリンクが含まれています。
結論: 「タイマーで一定間隔ごとに、一番近い敵を探して、弾を撃つ」。この3つを組み合わせるだけで、Brotato のような自動攻撃になります。この記事では Godot 4.7(無料のゲームエンジン)と GDScript(Godot 専用のプログラミング言語)で、動くところまで作ります。元ネタの Brotato も Godot 製のゲームです。
- 対象:Godot をインストール済みで、プロジェクトを新規作成できる人
- 使用バージョン:Godot 4.7 系(4.7.x)
1. 完成イメージ
- 矢印キーでアイコン画像のプレイヤーを動かせる
- 色を変えた敵が周囲から湧いて、プレイヤーに向かってくる
- 本記事の段階版では、何も操作しなくても一番近い敵に向かって弾が自動で飛び、当たった敵が消える
2. 仕組みの解説
- 一定間隔の攻撃:Timer
- 一番近い敵を探す:グループ
- 弾を撃つ:instantiate() によるシーンの複製
- 当たり判定:Area2D
- ノード:Godot の部品の最小単位。
- シーン:ノードを組み合わせて保存したもの。プレハブ(部品をテンプレート化したもの)に近い。
- シグナル:「〜が起きた」という合図。Timer の時間切れなどで発生する。
- インスペクタ:画面右側の、選んだノードの設定欄。
- ファイルシステム:画面左下の、プロジェクト内のファイル一覧。
3. 準備:プロジェクトを作る
この記事は英語のメニュー名を使い、シーンとスクリプトをプロジェクト直下に保存します。レンダラーは初期設定のままで始めます。
- Godot を起動し、プロジェクト一覧の画面で新しいプロジェクトを作成するボタンを押します。英語表示では「Create」です。
- 開いたダイアログで、プロジェクト名を
auto_attackなどにし、保存先フォルダを選びます。 - レンダラー(描画方式)は初期設定のままにし、ダイアログ下部の「Create」ボタンを押します。
- エディタが開いたら、表示言語を English(英語) にします。メニューの Editor → Editor Settings を開き、Interface → Editor → Editor Language で English を選びます。日本語表示を使う場合は、同じ役割の項目を選んでください。
- 以降、シーンとスクリプトはすべて
res://直下(フォルダを作らない場所)に保存します。
4. 弾(Bullet)を作る
4-1. シーンを作る
Area2D を使い、弾が敵に触れたことを検知します。
- 左上の「Scene」メニューから「New Scene」を選びます。
- 「Other Node」から
Area2Dを選んで追加します。 - ルートノード(シーンの一番上のノード)の名前を
Bulletに変更します。
4-2. 見た目と当たり判定を付ける
初期アイコンを小さくし、当たり判定の形を設定します。
Bulletを選んで子ノードにSprite2D(画像を表示するノード)を追加します。Sprite2Dの Texture 欄に、ファイルシステムのicon.svgをドラッグします。Sprite2Dの Scale(拡大率)の X と Y の両方を0.2にします。Bulletに子ノードCollisionShape2D(当たり判定の形を持つノード)を追加します。- インスペクタの Shape 欄で「New CircleShape2D」を選びます。半径は初期値のままで構いません。
4-3. スクリプトを書く
テンプレートのコードを、次のコードに置き換えます。
Bulletを右クリックして「Attach Script」を選びます。- 開いたダイアログのファイル名が
bullet.gdになっていることを確認し、「Create」を押します。 - コードエディタ内をクリックし、Ctrl+A で全選択してから、次のコードを貼り付けます。
class_name Bullet
extends Area2D
const SPEED := 600.0
var direction := Vector2.ZERO
func _ready() -> void:
get_tree().create_timer(3.0).timeout.connect(queue_free)
func _physics_process(delta: float) -> void:
position += direction * SPEED * delta
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("enemies"):
body.queue_free()
queue_free()
コードの読み方:
class_name Bullet:このスクリプトにBulletという型名を付けます。プレイヤー側からdirectionを安全に書き込むためです。const:変えない値(定数)です。SPEEDは弾の速さです。Vector2:縦横2つの数値の組です。位置や向きを表します。_ready:シーンに入った直後に1回だけ呼ばれる関数です。ここでは「3秒後に自分を削除する」予約をしています。当たらなかった弾が飛び続けて増えるのを防ぐためです。_physics_process:物理演算の更新ごと(初期設定で毎秒60回)に呼ばれる関数です。ここで弾を進めます。delta:前回の更新からの経過秒数です。これを掛けると、更新間隔が変わっても移動速度がおおむね一定になります。queue_free():ノードを安全に削除する命令です。
4-4. 当たり判定の合図をつなぐ
物体が重なったときに _on_body_entered が呼ばれるよう、シグナルを接続します。
Bulletを選び、右側の「Node」タブ →「Signals」を開きます。body_enteredをダブルクリックし、Bulletを選んで「Connect」を押します。- Ctrl+S で保存します。ファイル名を
bullet.tscnにして、保存先がres://直下であることを確認してから保存します。
5. 敵(Enemy)を作る
見下ろし視点に合わせて Floating モードを使い、敵の色を変えてプレイヤーと見分けます。
- 新規シーンで
CharacterBody2D(コードで動かす、物理演算つきのノード)を追加し、名前をEnemyにします。 - インスペクタで
Enemyの Motion Mode を「Floating」にします。 - 子に
Sprite2D(icon.svg、Scale は X・Y とも0.3)を追加します。 Sprite2Dを選び、インスペクタの CanvasItem → Visibility → Modulate(色を掛け合わせる設定)をクリックして、赤系の色を選びます。元の絵が青系なので、完全な赤にはならず、色味が変わります。- 子に
CollisionShape2Dを追加し、Shape に「New CircleShape2D」を選びます。次に、インスペクタに表示された CircleShape2D の欄をクリックして展開し、半径(Radius)を16にします。 Enemyを右クリックして「Attach Script」を選び、ファイル名がenemy.gdになっていることを確認して「Create」を押します。- コードエディタ内で全選択してから、次のコードを貼り付けます。
extends CharacterBody2D
const SPEED := 100.0
func _ready() -> void:
add_to_group("enemies")
func _physics_process(_delta: float) -> void:
var player: Node2D = get_tree().get_first_node_in_group("player")
if player == null:
return
velocity = (player.global_position - global_position).normalized() * SPEED
move_and_slide()
add_to_group("enemies"):弾とプレイヤーが「敵」を見つけるための名札です。global_position:画面全体を基準にした位置です。normalized():向きだけを残して長さを1にします。速度が一定になるためです。velocityとmove_and_slide():velocityに速さと向きを入れてmove_and_slide()を呼ぶと、その方向に動きます。get_tree():ゲーム全体のノードを扱うための入口です。ここから「グループ名で探す」ことができます。_delta:使わない引数は、先頭に_を付けると警告が出ません。
- Ctrl+S で、ファイル名を
enemy.tscnにして保存します(保存先はres://直下)。
6. プレイヤーを作る(自動攻撃の本体)
Camera2D はプレイヤーを追いかけます。移動中はプレイヤーが中央に留まり、敵や背景が動いて見えます。ゲーム画面をクリックしてから矢印キーを押し、敵や目印の位置を確認してください。攻撃用 Timer は自動開始にします。
- 新規シーンで
CharacterBody2Dを追加し、名前をPlayerにします。Motion Mode は「Floating」にします(理由は §5 と同じです)。 - 子に
Sprite2D(icon.svg、Scale は X・Y とも0.4)を追加します。色は変えず、そのままで構いません。 - 子に
CollisionShape2Dを追加し、Shape に「New CircleShape2D」を選んで、展開して半径(Radius)を20にします。 - 子に
Camera2Dを追加します。 - 子に
Timerを追加し、名前をAttackTimerにします。 AttackTimerのインスペクタで、Wait Time を0.8(0.8秒ごとに攻撃)にし、Autostart をオンにします。Playerを右クリックして「Attach Script」を選び、ファイル名がplayer.gdになっていることを確認して「Create」を押します。コードエディタ内で全選択してから、次のコードを貼り付けます。
extends CharacterBody2D
const SPEED := 300.0
const BULLET_SCENE := preload("res://bullet.tscn")
func _ready() -> void:
add_to_group("player")
func _physics_process(_delta: float) -> void:
var dir := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
velocity = dir * SPEED
move_and_slide()
func _on_attack_timer_timeout() -> void:
var target := _find_nearest_enemy()
if target == null:
return
var bullet: Bullet = BULLET_SCENE.instantiate()
get_parent().add_child(bullet)
bullet.global_position = global_position
bullet.direction = (target.global_position - global_position).normalized()
func _find_nearest_enemy() -> Node2D:
var nearest: Node2D = null
var nearest_dist := INF
for enemy in get_tree().get_nodes_in_group("enemies"):
var d := global_position.distance_squared_to(enemy.global_position)
if d < nearest_dist:
nearest_dist = d
nearest = enemy
return nearest
preload:ゲーム開始時にファイルを読み込んでおく命令です。ここでは弾のシーンを読み込んでいます。Input.get_vector(...):矢印キーの入力を上下左右の向きにまとめます(ui_leftなどは Godot に最初から入っている操作名です)。instantiate():シーンから実物のノードを1つ複製して作ります。distance_squared_to:距離の2乗を返します。大小比較だけなら平方根の計算が不要で軽いためです。INF:無限大を表す値です。最初に見つけた敵が、必ず「今までで一番近い」になります。add_childの後にglobal_positionを設定します。位置の設定は、シーンに追加したあとに行う形にそろえておくためです。
時間切れのたびに _on_attack_timer_timeout を呼ぶよう、シグナルを接続します。
- 「Node」タブ →「Signals」で
AttackTimerのtimeoutをPlayerに接続します。 - Ctrl+S で、ファイル名を
player.tscnにして保存します(保存先はres://直下)。
7. メインシーンで組み立てる
メインシーンにプレイヤーと敵を生成する Timer を配置します。
- 新規シーンで
Node2Dを追加し、名前をMainにします。 - ファイルシステムの
player.tscnをドラッグしてMainの子にします。位置は初期値(0, 0)のままで構いません。 Timerを追加し、名前をSpawnTimerにして、Wait Time を1.0、Autostart をオンにします。Mainを右クリックして「Attach Script」を選び、ファイル名がmain.gdになっていることを確認して「Create」を押します。コードエディタ内で全選択してから、次のコードを貼り付けます。
extends Node2D
const ENEMY_SCENE := preload("res://enemy.tscn")
@onready var player: Node2D = $Player
func _on_spawn_timer_timeout() -> void:
var enemy: Node2D = ENEMY_SCENE.instantiate()
add_child(enemy)
var angle := randf() * TAU
enemy.global_position = player.global_position + Vector2.from_angle(angle) * 700.0
@onready:シーンの準備ができてから、$Player(子ノードPlayer)を変数に入れる指定です。randf() * TAU:ランダムな角度です(TAU は円一周=約6.28)。プレイヤーから700ピクセル離れた円周上に敵が現れます。画面の大きさやカメラの設定によっては、画面内に現れる場合もあります。
このシーンを保存し、実行時にメインシーンとして選びます。
SpawnTimerのtimeoutをMainに接続します。- Ctrl+S で、ファイル名を
main.tscnにして保存します(保存先はres://直下)。 - F5 キーで実行します。メインシーンが未設定という確認が出たら、現在のシーンを選びます。
8. 動かないときのチェック
- 弾が出ない:
AttackTimerのtimeoutがPlayerに接続されているか確認します。 - 敵が動かない:
Playerがplayerグループに入っているか(_readyのadd_to_group("player"))確認します。 - 敵が消えない:
bullet.tscnのbody_enteredが接続されているか確認します。 - 弾が敵をすり抜ける:Bullet と Enemy の CollisionShape2D に Shape が設定されているか確認します。
- 起動時にエラーが出る:シーンとスクリプトを
res://直下に保存しているか確認します。
9. ハマった罠
Camera2D のせいで移動していないように見えた
Camera2D をプレイヤーの子に追加すると、プレイヤーは画面中央に追従します。背景や目印がない状態で矢印キーを押すと、プレイヤーの絵が画面内で動かないため、入力が効いていないように見えました。
ワールドの軸や敵の相対位置を確認すると、プレイヤーは実際には移動しています。検証用プロジェクトには、移動を確認しやすいよう横軸と縦軸を追加しました。
今回確認できた実例はこの1件です。未確認の失敗談を、数合わせで追加しないようにしました。
付属プロジェクトの検証結果(2026年10月17日)
検証時の auto_attack.zip を Godot 4.7 stable で確認しました。段階版ZIPは現在この記事からダウンロードできません。project.godot のメインシーン設定、シーンとスクリプトの読み込み、メインシーンの起動に問題はありませんでした。
ヘッドレス実行では、次の動作を確認しています。
AttackTimerが動き、一番近い敵へ弾を生成する- 弾が敵に当たると、敵が消える
- 敵がプレイヤーへ近づく
SpawnTimerによって敵が生成される
なお、これはコードと実行経路の確認です。エディタ上のクリック操作、画面の色や配置、矢印キーを使った手動操作は、別途 Godot の画面で確認してください。
2026年10月18日追記:プレイヤーの移動を画面で確認しやすくするため、検証済みZIPにはワールドの横軸・縦軸を追加しました。Camera2D がプレイヤーを追従するため、矢印キーを押すとプレイヤーではなく軸や敵の相対位置が動いて見えます。
10. まとめと次の一歩
- 自動攻撃は「Timer + 最も近い敵の検索 + 弾の生成」の組み合わせでできます。
- 次は、
AttackTimerの Wait Time を変えて攻撃速度を調整する、弾を複数同時に撃つ、といった拡張がおすすめです。 - このシリーズでは、Brotatoのようなアリーナ型ゲームで使われる仕組みを、Godotで一つずつ小さく再現していきます。
11. さらに学ぶ
今回の内容をもう少し体系的に学びたい場合は、Godot 4 と GDScript による2Dゲーム開発を扱う書籍も参考になります。
Godot 4 & GDScriptではじめる 2Dゲーム開発レシピ(Amazonで詳細を見る)
※本リンクはアフィリエイトリンクです。
統合版プロジェクトとの対応
この記事で扱う機能は、企画全体をまとめたauto_attack_final_verified.zipにも組み込んでいます。この記事のコードは段階的に追加するための説明なので、途中の記事と統合版全体のファイル構成は一致しない場合があります。
統合版には後続記事の機能も含まれます。記事の機能を確認するときは該当する操作を先に試し、最後にGodot 4.7でF5起動、入力、セーブ・ロード、再起動後の復元を確認してください。
