【Godot】Area2Dでコインを拾う方法|body_enteredでスコアを増やす
コインを拾ったら、1枚につき1点だけ増やす
Godotで「触れると消えるコイン」を作るなら、コインをArea2Dにして、プレイヤーが入ったときのbody_enteredを受け取ります。コインから取得シグナルを送り、ゲーム全体を管理するMainでスコアを増やせば、表示用Labelとコインを分けて扱えます。
この記事では、水色のキャラクターで3枚のコインを拾い、表示を「Score: 0」から「Score: 3」にする小さなサンプルを使います。壁が触れても取得されず、同じコインで二重に加算されないところまで確認します。
対象はGodot 4.6.3・GDScriptです。見下ろし型の2D移動で、重力・ジャンプ・セーブ機能は使いません。項目名は英語表記で説明します。コインを物理的な障害物にしない仕組みは、公式のArea2Dの説明でも紹介されています。
完成サンプルを開く
Area2Dでコインを拾う完成サンプル(ZIP)をダウンロードし、書き込み可能な場所へ展開してください。コード、シーン、キャラクターとコインのSVG画像、自動試験、README、MITライセンスを含めています。
- GodotのプロジェクトマネージャーでImportを選び、展開したフォルダーのproject.godotを指定します。
- インポートが終わったら、Run Projectで実行します。Windows・Linuxの標準ショートカットはF5です。
- ゲーム画面にフォーカスを移し、矢印キーで水色のPlayerを動かします。右に並んだコインを3枚拾うと、得点が0→1→2→3と増えます。
- 実行を停止してもう一度起動すると、コイン3枚・得点0から始まります。
以降はこのサンプルの設定を順に確認し、自分のプロジェクトへ組み込む場合の作り方を説明します。移動そのものを先に練習したい場合は、CharacterBody2Dで上下左右に移動して壁で止まる手順も参考にしてください。今回のPlayerは、その記事とCollision Layerの設定が異なります。

手順1:Player、Coin、スコア表示の役割を分ける
メインシーンはscenes/main.tscnです。主要なノードは次のようにつながります。Coinsは複数のコインをまとめるためのNode2Dで、その直下にはCoinシーンだけを置きます。
シーンツリーをPlainで示します。
Main (Node2D) … main.gd
├ Player (CharacterBody2D) … player.gd
│ ├ Sprite2D
│ └ CollisionShape2D
├ Coins (Node2D)
│ ├ Coin1 (Area2D) … coin.gd
│ │ ├ Sprite2D
│ │ └ CollisionShape2D
│ ├ Coin2 (Coinシーンのインスタンス)
│ └ Coin3 (Coinシーンのインスタンス)
└ CanvasLayer
└ ScoreLabel (Label)配布サンプルには、このほかに背景のArena、画面外へ出ないためのWalls、操作説明用のLabelがあります。コイン取得の処理を追うときは、上のノードだけに注目してください。

自分のプロジェクトに作る場合は、配布サンプルのassetsフォルダーを先にプロジェクト内へコピーします。Project SettingsのDisplay → WindowでViewport Widthを800、Viewport Heightを480にしてください。Node2DのMain、その子にCoinsとCanvasLayer、CanvasLayerの子にLabelを追加し、LabelをScoreLabelへ名前変更します。まずScoreLabelのTextに「Score: 0」を入れ、Positionを(44, 36)にすると、800×480の画面左上で確認できます。Main、Coins、CanvasLayer、ScoreLabelの名前は後のコードと一致させます。scenesフォルダーを作ってMainをres://scenes/main.tscnとして保存し、初回のRun Projectでメインシーンを選ぶ画面が出たらmain.tscnを指定します。
PlayerはCharacterBody2Dにする
scenes/player.tscnのルートはCharacterBody2Dです。自分で作る場合は新しいシーンのルートにCharacterBody2Dを置き、Playerへ名前変更してplayer.tscnとして保存します。子ノードとスクリプトを設定したあと、Mainの子へインスタンス化してください。子のSprite2Dにassets/player.svgを設定し、CollisionShape2DにはSizeが(36, 36)のRectangleShape2Dを設定しています。どちらの子ノードもPositionは(0, 0)、Scaleは(1, 1)です。PlayerのMotion ModeはFloatingにします。
プロジェクト内にscriptsフォルダーを作り、Playerへ新しいスクリプトを付けてres://scripts/player.gdとして保存してください。Playerの移動コードは次のとおりです。組み込みのui_left、ui_right、ui_up、ui_downを使うため、このサンプルではInput Mapへの新規登録は不要です。既存プロジェクトで組み込みアクションを変更している場合は、その入力設定も確認してください。
GDScript(構文色分けなし)のコードです。
extends CharacterBody2D
@export var speed: float = 220.0
func _physics_process(_delta: float) -> void:
var direction := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
velocity = direction * speed
move_and_slide()
メインシーンに配置したPlayerのPositionは(100, 280)です。移動のアクション名を変更する場合は、Input.get_vector()の引数も左・右・上・下の順にそろえます。
手順2:コインをArea2Dの独立したシーンにする
scenes/coin.tscnを開きます。ルートはCoinという名前のArea2Dで、子にSprite2DとCollisionShape2Dがあります。自分で作る場合もこの3ノードで構成し、coin.tscnとして保存します。
- Sprite2DのTexture:配布サンプル内のassets/coin.svg
- CollisionShape2DのShape:CircleShape2D
- CircleShape2DのRadius:22
- CollisionShape2DのDisabled:オフ
- Sprite2DとCollisionShape2DのPosition:(0, 0)、Scale:(1, 1)
- CoinのMonitoring:オン

画像は40×40ピクセルで、円の当たり判定を少し広くしています。Sprite2Dは見た目だけなので、画像を置くだけでは接触を検出できません。必ずCollisionShape2DへShapeを割り当てます。
ここで使うArea2Dは、重なったことを知らせる領域です。壁のようにプレイヤーを押し戻す必要がないため、コインにStaticBody2Dを使う必要はありません。
手順3:Collision Layer・Maskとグループを合わせる
ここが、コインに触れても反応しないときの確認ポイントです。Layerは自分が所属する層、Maskは自分が調べる相手の層を表します。今回はWorldを1番、Playerを2番として、次のように設定します。
- Player:Collision Layerは2番だけオン、Maskは1番だけオン
- Coin:Collision Layerはすべてオフ、Maskは2番だけオン
- Walls:Collision Layerは1番だけオン、Maskはすべてオフ
いずれもCollisionShape2Dではなく、Player・Coin・Wallsの本体側にあるCollision設定です。既定で入っている1番のチェックを残さないよう、選んだ番号以外はオフにします。

CoinはMaskの2番でPlayerを検出します。PlayerのMaskは壁の1番を見る設定のままで構いません。今回のようにCoin側で接触を調べる場合、Player側のMaskにCoin用の層を追加する必要はありません。所属と検出先の関係は、CollisionObject2Dの公式説明でも確認できます。
Inspectorの番号と、スクリプトやtscn内に書かれた整数は区別してください。整数はビットマスクで、1番だけなら1、2番だけなら2、3番だけなら4です。「Layerの3番」を指定したいときに、整数の3を設定すると1番と2番がオンになります。


Playerをplayersグループへ入れる
さらに、得点を取れる相手をplayersグループで確認します。Playerを選んでGroupsを開き、playersというグループを追加して所属させてください。配布サンプルではplayer.tscnに登録済みです。グループを付ける対象はSprite2DやCollisionShape2Dではなく、ルートのCharacterBody2Dです。

Layerは物理判定の候補を減らし、グループは「このゲームで取得を許す相手か」を判断します。同じ2番の層にいる別のキャラクターでも、playersに所属しなければコインを取得しません。大文字・小文字を含め、コードのplayersと名前を一致させます。
手順4:body_enteredから1回だけ取得を通知する
Coinへscripts/coin.gdを付け、内容を次のようにします。すでにサンプルを開いている場合は、同じ内容になっていることを確認してください。
GDScript(構文色分けなし)のコードです。
extends Area2D
signal collected
var _collected: bool = false
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
if _collected or not body.is_in_group("players"):
return
_collected = true
collected.emit()
queue_free()
_ready()のbody_entered.connect()で、接触時に呼ぶ関数を接続しています。今回はコードで接続するので、Signalsから同じ接続を追加しません。関数名を_on_body_enteredにしただけでは接続されない点にも注意してください。
bodyにはコインへ入ってきた相手が渡されます。引数はNode2Dとして受け取り、is_in_group(“players”)で対象を確認します。今回のPlayerはCharacterBody2Dなのでbody_enteredを使います。相手もArea2Dで構成している場合は、area_enteredを使う別の構成になります。
取得済みフラグを立ててからシグナルを出す
_collectedがtrueなら、以後の呼び出しは何もしません。順番は「取得済みにする→collectedを通知する→削除を予約する」です。

queue_free()は、その場でノードを消す処理ではありません。Nodeの公式説明のとおり、実際の削除は現在のフレームの末に行われます。削除されるまでに別の有効なbodyが同じコインへ接触しても、先に_collectedをtrueにしておけば得点通知は1回で止まります。
また、collectedを受け取った側の処理から取得関数が呼び直されても、フラグが先に立っていれば再度加算されません。「コインが消えるから大丈夫」と削除処理だけに頼らず、1枚を一度しか取得できない状態をコードで持たせています。
この実装では取得演出を待たず削除するので、MonitoringやShapeをコールバック内で変更しません。後から消えるまでの演出を追加し、物理コールバックからCollisionShape2Dを無効にしたい場合は、公式のDisabledの注意事項に従い、set_deferred()を使う方法を確認してください。
手順5:Mainでスコアを増やしてLabelへ表示する
Mainへscripts/main.gdを付けます。各コインのcollectedを同じ関数へ接続し、1枚取得するたびにscoreを1増やします。
GDScript(構文色分けなし)のコードです。
extends Node2D
var score: int = 0
@onready var score_label: Label = $CanvasLayer/ScoreLabel
func _ready() -> void:
for coin in $Coins.get_children():
coin.collected.connect(_on_coin_collected)
_update_score()
func _on_coin_collected() -> void:
score += 1
_update_score()
func _update_score() -> void:
score_label.text = "Score: %d" % score
$CanvasLayer/ScoreLabelは、このスクリプトを付けたMainから見た相対パスです。ScoreLabelを別の親へ移動した場合は、コードのパスも合わせます。Coin自身はこのLabelを知らず、取得を通知するだけです。
_ready()のfor文は、開始時点でCoinsの直下にあるコインを接続します。このコンテナーに別用途のNode2Dを混ぜるとcollectedが見つからなくなるため、直下はCoinシーンだけにそろえてください。ゲームの途中で新しいコインを生成する仕組みを追加するなら、そのコインにも生成時の接続処理が必要です。
表示には”Score: %d” % scoreを使います。scoreを増やすだけではLabelの文字は変わらないため、取得後に_update_score()を呼びます。_ready()でも呼ぶので、起動直後は0になります。
コインを3個置く
自分のメインシーンで作っている場合は、Coinsの子にcoin.tscnを3個インスタンス化します。名前をCoin1、Coin2、Coin3とし、Positionをそれぞれ(260, 280)、(420, 280)、(580, 280)にします。Coins自身のPositionは(0, 0)のままです。
Coinのコードは同じでも、_collectedは各インスタンスが別々に持ちます。1枚目の取得で2枚目や3枚目まで取得済みになることはありません。
手順6:取得・表示・二重加算を確認する
保存してメインシーンを実行し、次の順で確認します。
- 起動直後にScore: 0と3枚のコインがある
- 1枚に触れると、その1枚だけ消えてScore: 1になる
- 通過後もプレイヤーを動かせる
- 残りの2枚を拾うとScore: 3になる
- 停止・再実行するとScore: 0へ戻る

ゲーム内のテストを増やす場合は、Playerと同じLayerのbodyでもplayersグループから外した相手では取得されないか、2つの取得可能なbodyを同じコインへ重ねても1点だけ増えるかを確認します。ひとつのbodyにShapeを増やすことと、複数のbodyを接触させることは別の条件です。
反応しない・加算がおかしいときの確認箇所
触れてもコインが消えない
- Debuggerにコードやノードパスのエラーがないか確認します。
- CoinのMaskが2番、PlayerのLayerが2番になっているか確認します。
- CoinのMonitoringがオンか確認します。
- 両方のCollisionShape2DにShapeがあり、Disabledがオフか確認します。
- Player本体がplayersグループに入っているか確認します。
- Coinにcoin.gdが付き、_ready()の接続行が残っているか確認します。
Monitoringは「このAreaが相手を検出するか」です。Monitorableは「ほかのAreaからこのAreaを検出できるか」で、意味が異なります。今回のCoinがPlayerを検出するために見る項目はMonitoringです。
見た目と判定がずれている場合は、実行時のDebug → Visible Collision Shapesで判定を表示し、Sprite2DとCollisionShape2Dの位置・サイズを見比べます。デバッグ表示は確認が終わったら戻してください。
コインは消えるのにスコアが変わらない
消えるところまで動くなら、接触の検出とCoin側の処理は進んでいます。次にMainの_ready()が各コインのcollectedを接続しているか確認します。CoinがCoinsの直下にいない場合は、現在のfor文で接続されません。
scoreは増えるのに表示だけ変わらない場合は、$CanvasLayer/ScoreLabelの名前と親子関係、_update_score()の呼び出しを確認します。CoinシーンだけをRun Current Sceneで実行しても、MainとScoreLabelは存在しません。今回の動作確認にはRun Projectでmain.tscnを実行してください。
1枚で2点以上増える・接続済みエラーが出る
_collected = trueがcollected.emit()より前にあるか、同じ得点加算を別の関数でも行っていないか確認します。また、同じ位置にCoinのインスタンスを2枚置くと、1枚ずつ正常に取得して合計2点になります。Remoteのノードツリーなどで、実際のコイン数も調べてください。
同じシグナルを同じ関数へエディターとコードの両方から接続すると、接続済みエラーの原因になります。今回のサンプルはコード接続に統一しています。
起動直後だけ重なり判定を取れない
get_overlapping_bodies()の情報は、ノードを置いた直後にその場で更新されるわけではありません。公式リファレンスのとおり物理更新のタイミングに依存します。今回のコードは生成直後の一覧取得に頼らず、body_enteredを受け取る構成です。
サンプルの検証範囲とライセンス
配布プロジェクトはLinuxのGodot 4.6.3 stable officialでheadless実行し、38項目の自動試験を通しています。通常と逆順の取得、Labelの文字列、コイン削除、2つの有効bodyの同時接触、グループによる除外、Mask・Monitoring・Shapeの設定不備と復旧、入力アクションによる移動と壁停止を確認しました。
さらに、同じLinux環境のGodotエディターでプロジェクトを開き、矢印キーによる上下左右の移動、Score: 0→1→2→3、コイン消失、壁停止、停止後の再実行で0へ戻ることを確認しました。本文のゲーム画面とInspector画像は、この実行時に採取したものです。レンダラーはCompatibility(Mesa llvmpipe)です。
検証環境では、起動時にV-Sync非対応、音声デバイスを開けない旨、音声のdummy driverへの切り替えの3件がDebuggerに表示されました。コイン取得のスクリプトエラーは確認していませんが、GUI全体がエラー0という結果ではありません。音を扱わないサンプルとして表示と操作を確認しており、音声再生とWindows・macOS・モバイルへの書き出しは未検証です。
コード、シーン、同梱SVGはMIT Licenseで利用・改変・商用利用・再配布できます。再配布時はZIP内のLICENSE.txtを残してください。Godot本体と公式ドキュメント、この記事の本文・アイキャッチ画像はZIPのライセンス対象に含みません。
まとめ
コイン取得では、Area2Dのbody_enteredで接触を検出し、Layer・Maskとグループで相手を絞ります。そのうえで取得済みフラグを先に立て、collectedを通知してから削除を予約すると、1枚につき1回だけ得点を渡せます。
取得ができたら、次は待機・移動アニメーションと左右反転を加えてみてください。操作や機能から別の記事を探す場合は、Godotの機能別ガイドから選べます。
サイトアイコン-2-150x150.png)