※本記事にはアフィリエイトリンクが含まれています。
Amazonのアソシエイトとして、ゲームのタネは適格販売により収入を得ています。
※一部リンクは楽天アフィリエイトを含みます。
▶ この回の完成形を、この場で遊ぶ
20秒で、出た的をクリックして得点を稼ぐゲームです。逃げられる前に tap。空白は無効です。
Godotで作る完成形と同じルールです。はじめる(または画面クリック、R)で開始します。手順は下に続きます。
Godotの完成プロジェクト
ブラウザで遊んでいるものと同じゲームです(はじめる、円の的、得点、20秒、終了、もう一度)。ZIPを解凍し、Godot 4.7(4.3以上)で project.godot を開いて F5。
click_targets.zip をダウンロード
はじめに
「エンジンは入れたけれど、画面のものを自分のクリックで消す、という最初の一手が分からない」——そんな声をよく聞きます。
この記事では、Godot 4.7(執筆時点の安定版は 4.7.2)で、20秒的あてを1本作って完成させます。的は自分で出てきて、短い時間で消えます。出ているあいだにクリックすると1点、時間切れで終了です。自己ベストを更新するために、何度でもやり直せます。
今週身につけること:入力とオブジェクト。 マウスのクリックを「どのオブジェクトが受け取ったか」まで追えるようになります。制限時間と得点は、それがゲームになるための枠です。
完成形と、やらないこと
- 作るもの: 20秒間、的がランダムに出現する。クリックで1点。逃すと得点は増えない。時間切れで終了、Rでやり直し。
- やらないこと: 画像素材の作り込み、音、ランキングサーバー、スマホ専用UI。
- 持ち帰る1アイデア: クリックは「画面全体のイベント」ではなく、
Area2D(範囲を持つオブジェクト)が受け取れる。


準備(プロジェクト)
- Godot公式ダウンロードから安定版(4.7系)を入れます。まだの方は、サイトの導入記事もどうぞ。
- 完成一式から始める場合は、上の ZIP を解凍して
project.godotをインポートします。自分で組む場合は次です。プロジェクトマネージャーで「新規プロジェクト」→名前は例としてclick_targets。レンダラーは初心者なら「互換性」で十分です。 - 2Dシーンを開き、ルートを
Node2Dにして名前をMainにします。保存先はres://main.tscn。プロジェクト設定でメインシーンに指定します。
的オブジェクトを1つ作る
的は「見た目」と「クリックを拾う範囲」をセットで持ちます。Godotではよく、Area2D の子に CollisionShape2D(当たりの形)と Polygon2D(色付きの見た目)を置きます。

- 新しいシーンを作り、ルートを
Area2D。名前はTarget。保存はres://target.tscn。 - 子ノードを追加:
CollisionShape2D。Inspector の Shape に New →CircleShape2D。Radius は 36 程度(見た目の円と揃えます)。 - もう1つ子を追加:
Polygon2D。Polygon に矩形か円に近い頂点を入れ、色を好みで変更します(例:明るい赤)。見た目の大きさと円の半径がだいたい一致するようにします。 - ルートの
Area2Dで、Input Pickable がオン、Collision → Layer に少なくとも1ビットが入っていることを確認します(初期値のままで通常は問題ありません)。公式ドキュメントでも、ピックにはinput_pickableと collision layer の両方が必要と書かれています。
スクリプト1:target.gd(Area2D に付ける)
ファイル名:res://target.gd。Target(Area2D)にアタッチします。
extends Area2D
## 消えたことを親(Main)に知らせる
signal cleared
const LIFE := 1.15
const COLORS := [
Color(0.910, 0.365, 0.298),
Color(0.941, 0.635, 0.008),
Color(0.239, 0.733, 0.604),
Color(0.357, 0.553, 0.937),
Color(0.780, 0.490, 1.000),
]
var life: float = LIFE
func _ready() -> void:
_make_circle()
get_tree().create_timer(LIFE).timeout.connect(queue_free)
func _process(delta: float) -> void:
life -= delta
var k: float = maxf(0.35, life / LIFE)
modulate.a = 0.45 + 0.55 * k
scale = Vector2.ONE * (0.85 + 0.15 * k)
func _make_circle() -> void:
var poly := $Polygon2D
var pts := PackedVector2Array()
for i in 24:
var a := TAU * float(i) / 24.0
pts.append(Vector2(cos(a), sin(a)) * 36.0)
poly.polygon = pts
poly.color = COLORS[randi() % COLORS.size()]
func _input_event(_viewport: Viewport, event: InputEvent, _shape_idx: int) -> void:
if event is InputEventMouseButton \
and event.pressed \
and event.button_index == MOUSE_BUTTON_LEFT:
cleared.emit()
queue_free()
別ゲームに持ち替えるときの変更点: 左クリック以外にしたいなら MOUSE_BUTTON_RIGHT やキー入力に差し替えます。消す代わりにダメージを与えたいなら queue_free() の前に自分のHPを減らす処理を挟みます。
なぜ _input_event か:画面全体の _input でマウス座標と的の位置を毎回計算するより、「このオブジェクトの形の上で起きた入力」だけを公式が渡してくれます(CollisionObject2D の仮想メソッド/input_event シグナル)。
メインシーンで出現・得点・制限時間を回す
main.tscnのMainの下に、盤面色のPolygon2D(名前Board、画面のバーより下)と、空のNode2D(名前Targets)を置きます。的はあらかじめ置かず、コードから出します。BoardをCanvasLayerの色付き矩形にすると的が隠れます。CanvasLayerを追加し、名前をUIにします。子に上部バー(HBoxContainer名Bar)を置き、その中にLabel(RemainingLabel)と黄色いButton(StartButton、テキスト「はじめる」)。盤面の上にControl(Overlay)と2つの Label(Title/Sub)を置きます。完成ZIPではこのUIまで入っています。main.gdを付けます。的シーンはpreload("res://target.tscn")で読むので、Inspector へのドラッグは不要です。

スクリプト2:main.gd(Main に付ける)
ファイル名:res://main.gd。ルートの Main にアタッチします。
extends Node2D
const LIMIT := 20.0
const SPAWN_EVERY := 0.55
const MAX_ON_SCREEN := 3
const BOARD := Vector2(720, 450)
const BOARD_TOP := 56.0
@onready var remaining_label: Label = $UI/Bar/RemainingLabel
@onready var start_button: Button = $UI/Bar/StartButton
@onready var overlay_title: Label = $UI/Overlay/Title
@onready var overlay_sub: Label = $UI/Overlay/Sub
@onready var overlay: Control = $UI/Overlay
@onready var targets: Node2D = $Targets
var target_scene: PackedScene = preload("res://target.tscn")
var score: int = 0
var best: int = 0
var time_left: float = LIMIT
var spawn_acc: float = 0.0
var state: String = "ready"
func _ready() -> void:
start_button.pressed.connect(_start_game)
overlay.gui_input.connect(_on_board_gui_input)
_show_ready()
func _process(delta: float) -> void:
if state != "play":
return
time_left -= delta
spawn_acc += delta
if spawn_acc >= SPAWN_EVERY:
spawn_acc = 0.0
_spawn()
if time_left <= 0.0:
time_left = 0.0
_end_game()
return
_refresh_hud()
func _start_game() -> void:
state = "play"
score = 0
time_left = LIMIT
spawn_acc = 0.0
for child in targets.get_children():
child.queue_free()
overlay.visible = false
overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
start_button.text = "やり直す(R)"
_spawn()
_spawn()
_refresh_hud()
func _end_game() -> void:
state = "over"
for child in targets.get_children():
child.queue_free()
if score > best:
best = score
remaining_label.text = "終了 得点 %d 自己ベスト %d" % [score, best]
start_button.text = "もう一度(R)"
overlay_title.text = "得点 %d" % score
overlay_sub.text = "もう一度でベストを更新"
overlay.mouse_filter = Control.MOUSE_FILTER_STOP
overlay.visible = true
func _show_ready() -> void:
state = "ready"
remaining_label.text = "20秒的あて はじめる でスタート"
start_button.text = "はじめる"
overlay_title.text = "20秒で、出た的をクリック"
overlay_sub.text = "逃げられる前に tap。空白は無効"
overlay.mouse_filter = Control.MOUSE_FILTER_STOP
overlay.visible = true
func _spawn() -> void:
if targets.get_child_count() >= MAX_ON_SCREEN:
return
var t := target_scene.instantiate()
t.position = Vector2(
randf_range(80.0, BOARD.x - 80.0),
randf_range(80.0, BOARD.y - 80.0) + BOARD_TOP
)
if t.has_signal("cleared"):
t.cleared.connect(_on_hit)
targets.add_child(t)
func _on_hit() -> void:
if state == "play":
score += 1
_refresh_hud()
func _refresh_hud() -> void:
remaining_label.text = "得点 %d 残り %d 秒" % [score, ceili(time_left)]
func _on_board_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
if state == "ready" or state == "over":
_start_game()
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo:
if event.keycode == KEY_R:
_start_game()
preload("res://target.tscn") は、的シーンを何度でも複製するための読み込みです。パス $UI/Bar/RemainingLabel と $UI/Bar/StartButton、$UI/Overlay がシーンと一致しているか確認してください。完成ZIPではこのUIまで入っています。
別ゲームへのリネーム例: Targets → Moles、cleared → whacked。出現と得点の枠はそのまま使えます。
Playして確認すること
- F5(または Play)で実行します。最初は止まります。はじめる(または盤面クリック、R)でスタートします。
- 的が次々と出ます。的の上で左クリックすると1点入ります。
- クリックしない的は約1.15秒で消えます。空白をクリックしても得点は増えません。
- 20秒で「終了」と自己ベストが出ます。もう一度または R でやり直しです。
「動けばOK」ではなく、次を確認してください。空白をクリックしても得点が増えないこと。これが「オブジェクトが入力を受け取っている」証拠です。

よくあるつまづき(1つ)
症状: 得点と残り時間は動くのに、盤面に的が見えない。
気づき方: CanvasLayer の子に盤面いっぱいの ColorRect があると、その下の Node2D(的)が隠れます。盤面の色は Main 直下の Polygon2D(名前 Board)に置き、UIレイヤーには上部バーと開始オーバーレイだけを置きます。
症状: Playしてもクリックしても的が消えない。
気づき方: シーンドックで CollisionShape2D を選び、Inspector の Shape が空(<empty>)になっていないか見ます。形が無いとピックできません。あわせて Area2D の Input Pickable がオフ、または Collision Layer がすべてオフ、になっていないかも確認します。
できたこと/身についたこと
- Area2D+CollisionShape2D で「クリックできるオブジェクト」を表現できた。
_input_eventでマウスボタンを判定し、queue_free()で消せるようになった。- 親が出現・制限時間・得点を回し、時間切れと もう一度(R)によるやり直しまでつながった。
使い回しメモ
- スニペット:
target.gdの_input_event一式。 - エンコードしている考え: 「入力はオブジェクトが拾う」。
- 次の使い道: シューティングの敵をクリック/タップで撃つ、メニューの透明なボタン領域、マップ上の建物を選ぶ。
次の自分のゲームで使うなら
ジャンルが違っても、「画面のどこかをクリックした」ではなく「どのノードの形の上か」を先に決めると同じ詰まりを避けられます。次は同じ的あてを Unity で作り、コンポーネントの分け方の違いを見ると理解が定着します。
次に足してよい一言
的が消えるたびに短い音を1つ足す、または連続ヒットで得点が伸びる、くらいが次の小さな一歩です。
関連商品(PR)
この回はクリック操作が中心なので、続きの学習用の本と、操作しやすいマウス/パッドだけ短く挙げます。価格は店舗で確認してください。
- ゆるっとはじめる Godot Engine ゲームプログラミング(杉山悠真)
Amazonで見る
※本リンクはアフィリエイトリンクです。 - Logicool G304 ワイヤレスゲーミングマウス(クリック操作の練習にも扱いやすい定番)
Amazonで見る / 楽天市場で見る
※本リンクはアフィリエイトリンクです。 - ゲーミングマウスパッド(広いパッド例と標準クロス例)
Amazonで見る(Pulsar XXL例) / 楽天市場で見る(Logicool G240f)
※本リンクはアフィリエイトリンクです。
参考
クラスやメソッド、Godotのバージョンは、次の公式ページで確認しています。
- Godot Engine 公式ダウンロード
- CollisionObject2D(
_input_event、input_pickable) - Area2D
- InputEventMouseButton
- Node(
queue_free()) - CanvasLayer(盤面をUIレイヤーの色付き矩形で覆うと、的が隠れます)


コメント