コインを拾ったら、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ライセンスを含めています。

  1. GodotのプロジェクトマネージャーでImportを選び、展開したフォルダーのproject.godotを指定します。
  2. インポートが終わったら、Run Projectで実行します。Windows・Linuxの標準ショートカットはF5です。
  3. ゲーム画面にフォーカスを移し、矢印キーで水色のPlayerを動かします。右に並んだコインを3枚拾うと、得点が0→1→2→3と増えます。
  4. 実行を停止してもう一度起動すると、コイン3枚・得点0から始まります。

以降はこのサンプルの設定を順に確認し、自分のプロジェクトへ組み込む場合の作り方を説明します。移動そのものを先に練習したい場合は、CharacterBody2Dで上下左右に移動して壁で止まる手順も参考にしてください。今回のPlayerは、その記事とCollision Layerの設定が異なります。

Godotサンプルの実行直後。Score: 0と水色のPlayer、3枚のコイン
実行直後の画面。矢印キーで水色のPlayerを動かします。

手順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があります。コイン取得の処理を追うときは、上のノードだけに注目してください。

Main、Player、Coins、CanvasLayerとScoreLabelの関係を示す概念図
主要ノードの構成と役割。概念図であり、エディターのスクリーンショットではありません。

自分のプロジェクトに作る場合は、配布サンプルの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:オン
CoinのCollisionShape2Dを選択し、Radius 22とDisabledオフを確認した実画面
Coinの実画面。円の判定はRadius 22、Disabledはオフです。

画像は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のLayer 2を検出し、PlayerのMask 1がWorldのLayer 1を調べる図
このサンプルのLayerとMask。番号はInspectorのチェック項目です。

CoinはMaskの2番でPlayerを検出します。PlayerのMaskは壁の1番を見る設定のままで構いません。今回のようにCoin側で接触を調べる場合、Player側のMaskにCoin用の層を追加する必要はありません。所属と検出先の関係は、CollisionObject2Dの公式説明でも確認できます。

Inspectorの番号と整数を区別する

Inspectorの番号と、スクリプトやtscn内に書かれた整数は区別してください。整数はビットマスクで、1番だけなら1、2番だけなら2、3番だけなら4です。「Layerの3番」を指定したいときに、整数の3を設定すると1番と2番がオンになります。

CoinのInspectorでMonitoringがオン、Collision Layerはすべてオフ、Maskは2だけオンの実画面
Coin本体の設定。Monitoringをオンにして、Maskの2番だけを選びます。
PlayerのInspectorでMotion ModeがFloating、Layer 2、Mask 1になっている実画面
PlayerはLayerの2番、Maskの1番をオンにしています。

Playerをplayersグループへ入れる

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

Player本体を選択しGroupsタブのScene Groupsでplayersがチェックされている実画面
Player本体を選択し、Groupsタブでplayersへの所属を確認します。

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を通知する→削除を予約する」です。

body_entered、対象確認、取得済みフラグ、collected通知、queue_freeの順に処理する図
取得済みフラグを先に立て、得点の通知を1回に限定します。

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:取得・表示・二重加算を確認する

保存してメインシーンを実行し、次の順で確認します。

  1. 起動直後にScore: 0と3枚のコインがある
  2. 1枚に触れると、その1枚だけ消えてScore: 1になる
  3. 通過後もプレイヤーを動かせる
  4. 残りの2枚を拾うとScore: 3になる
  5. 停止・再実行するとScore: 0へ戻る
Godotで3枚のコインを拾った後の実行画面。Score: 3になりコインが消えている
3枚を取得した実画面。Score: 3になり、コインがすべて消えました。

ゲーム内のテストを増やす場合は、Playerと同じLayerのbodyでもplayersグループから外した相手では取得されないか、2つの取得可能なbodyを同じコインへ重ねても1点だけ増えるかを確認します。ひとつのbodyにShapeを増やすことと、複数のbodyを接触させることは別の条件です。

反応しない・加算がおかしいときの確認箇所

触れてもコインが消えない

  1. Debuggerにコードやノードパスのエラーがないか確認します。
  2. CoinのMaskが2番、PlayerのLayerが2番になっているか確認します。
  3. CoinのMonitoringがオンか確認します。
  4. 両方のCollisionShape2DにShapeがあり、Disabledがオフか確認します。
  5. Player本体がplayersグループに入っているか確認します。
  6. 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の機能別ガイドから選べます。