【Godot】Area2Dで作る範囲検出とトリガー

作成: 2025-12-08最終更新: 2026-07-07

GodotのArea2Dを使って、アイテム取得、敵の索敵範囲、イベント開始、毒沼の継続ダメージなどを実装する方法を、シグナルとレイヤー/マスクの考え方から解説します。

アイテムに近づいたら拾う。敵の索敵範囲に入ったら追いかけられる。毒沼に立っている間だけHPが減る。イベントエリアに入ったら会話が始まる。

こうした「ぶつかって止めたいわけではないけれど、範囲に入ったことは知りたい」処理に使うのが Area2D です。

PhysicsBody2Dは移動を止め、Area2Dは通り抜けながら入ったことを検出する図

この記事でわかること

  • Area2Dが「通り抜けるセンサー」である理由
  • body_entered / body_exited / area_entered の使い分け
  • 敵の索敵範囲、毒沼、アイテム取得の実装例
  • Collision Layer / Maskで検出対象を絞る考え方

Sponsored

Area2Dは「衝突」ではなく「検出」

Area2D は、物理的に押し返すためのノードではありません。キャラクターがArea2Dへ入っても、Area2D自体は壁のように動きを止めません。

ノード主な役割プレイヤーの移動を止めるか
StaticBody2D床、壁、固定障害物止める
CharacterBody2Dプレイヤー、敵、NPC動かし方次第で衝突する
RigidBody2D箱、ボール、物理オブジェクト物理的に押し合う
Area2Dセンサー、範囲、トリガー止めない

Area2Dは、ゲーム内では次のような用途に向いています。

  • アイテム取得範囲
  • 敵の索敵範囲
  • 攻撃判定
  • 毒沼や回復エリア
  • ワープゾーン
  • 会話やカットシーン開始トリガー

「触れたら反応したい」ならArea2D、「ぶつかって止めたい」なら物理ボディ、と考えると選びやすいです。

Area2Dの基本構成

Area2Dを機能させるには、子に CollisionShape2D または CollisionPolygon2D が必要です。Area2D本体だけを置いても、検出範囲がないため反応しません。

PickupArea (Area2D)
└─ CollisionShape2D

基本の流れは次の通りです。

  1. Area2D を作る
  2. 子に CollisionShape2D を追加する
  3. 検出したい範囲の形を設定する
  4. body_entered などのシグナルを接続する
  5. 必要ならCollision Layer / Maskを設定する

Area2Dは見えない範囲として使うことが多いため、エディタ上でCollisionShape2Dの形を見ながら調整します。

Sponsored

bodyとareaのシグナルを使い分ける

Area2Dで最初に迷いやすいのが、body_enteredarea_entered の違いです。

シグナル何が入ったときに呼ばれるか
body_entered(body)CharacterBody2DRigidBody2DStaticBody2D などプレイヤーがアイテム範囲に入る
body_exited(body)物理ボディが範囲から出るプレイヤーが索敵範囲から出る
area_entered(area)別の Area2D攻撃判定が敵の HurtBox に当たる
area_exited(area)別の Area2D が範囲から出るセンサー同士の重なり終了

Area2Dのシグナルは、入った瞬間と出た瞬間を扱うのに向いています。範囲内にいる間ずっと処理したい場合は、入った対象を配列に入れ、出たら配列から外し、_physics_process() で処理する形が分かりやすいです。

Area2Dのbody_enteredとbody_exitedでtargetsリストへ追加・削除する図
extends Area2D

var targets: Array[Node2D] = []

func _on_body_entered(body: Node2D) -> void:
    if body.is_in_group("player") and not targets.has(body):
        # 入った瞬間にリストへ追加する
        targets.append(body)

func _on_body_exited(body: Node2D) -> void:
    if targets.has(body):
        # 出た瞬間にリストから外す
        targets.erase(body)

この形にすると、「入った瞬間の処理」と「範囲内にいる間の処理」を分けて考えられます。

実装例1: 敵の索敵範囲

敵の子に Area2D を置くと、索敵範囲を作れます。

Enemy (CharacterBody2D)
├─ Sprite2D
├─ CollisionShape2D
└─ SightArea (Area2D)
   └─ CollisionShape2D

SightArea は敵本体の衝突とは別です。プレイヤーが索敵範囲に入っても、そこで止まるわけではありません。入ったことを検出して、敵AIの状態を変えます。

SightAreaに入ったプレイヤーをbody_enteredでtargetに設定し、追跡開始へ進む図
# enemy.gd
extends CharacterBody2D

@onready var sight_area: Area2D = $SightArea

var target: Node2D = null
var candidates: Array[Node2D] = []

func _physics_process(delta: float) -> void:
    if candidates.is_empty():
        target = null
        return

    # 範囲内に複数いる場合は、毎フレーム一番近い対象を選ぶ
    target = find_closest_target()

    if target != null:
        chase_target(target, delta)

func _on_sight_area_body_entered(body: Node2D) -> void:
    if body.is_in_group("player") and not candidates.has(body):
        # 索敵範囲に入った候補を覚える
        candidates.append(body)

func _on_sight_area_body_exited(body: Node2D) -> void:
    if candidates.has(body):
        # 範囲外に出たら候補から外す
        candidates.erase(body)

func find_closest_target() -> Node2D:
    var closest: Node2D = null
    var closest_distance_sq := INF

    for candidate in candidates:
        if not is_instance_valid(candidate):
            continue

        var distance_sq := global_position.distance_squared_to(candidate.global_position)
        if distance_sq < closest_distance_sq:
            closest_distance_sq = distance_sq
            closest = candidate

    return closest

func chase_target(target_body: Node2D, delta: float) -> void:
    var direction := global_position.direction_to(target_body.global_position)
    velocity = direction * 120.0
    move_and_slide()

この例では distance_squared_to() を使っています。距離比較だけなら平方根を取る必要がないため、distance_to() より軽く済みます。とはいえ、数体の敵なら大きな差は気にしなくて大丈夫です。大事なのは、Area2Dが敵の移動を止めるのではなく、AIへ「見つけた」と知らせる役割を持つことです。

Sponsored

実装例2: 毒沼の継続ダメージ

毒沼や炎上床のように、範囲内にいる間ずっと効果を与えたい場合があります。

この場合、body_entered だけでは足りません。body_entered は入った瞬間だけ呼ばれるためです。

DamageAreaが毎物理フレームでget_overlapping_bodies()を使い、take_damage(delta)を呼ぶ図

小規模なエリアなら、get_overlapping_bodies() を使うと分かりやすいです。

# damage_area.gd
extends Area2D

@export var damage_per_second := 8.0

func _physics_process(delta: float) -> void:
    for body in get_overlapping_bodies():
        if body.is_in_group("player") and body.has_method("take_damage"):
            # 1秒あたりのダメージをdeltaで割り振る
            body.take_damage(damage_per_second * delta)

このコードは読みやすく、毒沼が数個ある程度なら十分です。

一方で、同じようなDamageAreaが大量にある場合は、毎フレーム get_overlapping_bodies() を呼ぶより、入った/出た対象をリスト管理する方が見通しも性能も安定しやすいです。

extends Area2D

@export var damage_per_second := 8.0
var bodies_inside: Array[Node2D] = []

func _physics_process(delta: float) -> void:
    for body in bodies_inside:
        if is_instance_valid(body) and body.has_method("take_damage"):
            # 範囲内にいる間だけ継続ダメージを与える
            body.take_damage(damage_per_second * delta)

func _on_body_entered(body: Node2D) -> void:
    if body.is_in_group("player") and not bodies_inside.has(body):
        bodies_inside.append(body)

func _on_body_exited(body: Node2D) -> void:
    bodies_inside.erase(body)

どちらが正解というより、用途で選びます。小さなプロトタイプでは get_overlapping_bodies()、多くの範囲が出るゲームではリスト管理、という考え方で始めるとよいです。

実装例3: アイテム取得トリガー

アイテム取得は、Area2Dの最も分かりやすい使いどころです。

Coin (Node2D)
├─ Sprite2D
└─ PickupArea (Area2D)
   └─ CollisionShape2D
# coin.gd
extends Node2D

@export var score_value := 10
@onready var pickup_area: Area2D = $PickupArea

func _on_pickup_area_body_entered(body: Node2D) -> void:
    if not body.is_in_group("player"):
        return

    if body.has_method("add_score"):
        # 取得した相手へスコア加算を依頼する
        body.add_score(score_value)

    queue_free()

このように、Area2Dは「プレイヤーが範囲に入った」ことだけを検出します。スコア加算をプレイヤーに持たせるのか、Autoloadの PlayerData.add_score() に寄せるのかは、プロジェクト設計に合わせて選びます。

レイヤー/マスクで検出対象を絞る

Area2Dが反応しない、または関係ない敵やアイテムに反応してしまう場合、Collision Layer / Maskの設定を見直します。

基本は次の関係です。

検出する側の Mask が、検出される側の Layer を見ていると反応する。

Area2DのMaskがPlayerレイヤーだけを検出し、EnemyレイヤーとItemレイヤーを除外する図

たとえば、プレイヤーだけを検出したい索敵範囲なら、プレイヤーを Player Layer に置き、SightArea のMaskを Player Layer だけにします。

この設定をしておくと、Godotの物理側で不要な検出を減らせます。コード内で is_in_group("player") を書くこともありますが、レイヤー/マスクで先に絞っておくと、そもそも余計なシグナルが飛びにくくなります。

monitoringとmonitorable

Area2Dには monitoringmonitorable もあります。

プロパティ意味
monitoringこのArea2Dが他を検出するか敵の索敵範囲はtrue
monitorableこのArea2Dが他のArea2Dから検出される対象になるかHurtBoxやアイテム判定

多くの初心者向け実装では、まずデフォルトのままでも動きます。うまく動くようになってから、不要な検出を減らしたい場面で見直すとよいです。

Sponsored

よくあるつまずき

CollisionShape2Dを付け忘れる

Area2D本体だけでは範囲がありません。子に CollisionShape2D を置き、Shapeを設定してください。

body_enteredが呼ばれない

まず次を確認します。

  • monitoring が有効か
  • Area2DのMaskが、相手のLayerを見ているか
  • 相手が CharacterBody2DRigidBody2D などのbodyか
  • CollisionShape2D が無効化されていないか

相手もArea2Dなら、body_entered ではなく area_entered を使います。

範囲内にいる間の処理をbody_enteredだけで書く

body_entered は入った瞬間だけです。毒沼や回復エリアのように継続処理が必要なら、get_overlapping_bodies() または入退場シグナルで管理した配列を _physics_process() で処理します。

Area2Dを壁として使おうとする

Area2Dは移動を止めません。壁や床として使いたいなら StaticBody2D、動くキャラクターなら CharacterBody2D を使います。

おまけ: 先に知っておくと良いこと

HitBoxとHurtBoxを分けると攻撃判定が整理しやすい

アクションゲームでは、攻撃する範囲を HitBox、ダメージを受ける範囲を HurtBox として、どちらもArea2Dで作ることがあります。

Player
├─ HurtBox (Area2D)
└─ SwordHitBox (Area2D)

この形にすると、体に当たったか、剣に当たったか、敵の攻撃範囲に入ったかを分けやすくなります。後続のCollision Layers/Masks記事と組み合わせると、味方の攻撃は味方に当たらない、敵の攻撃だけ受ける、といった設定が作りやすくなります。

RayCast2DやShapeCast2Dとの違い

Area2Dは「範囲に入ったもの」を検出します。視線の先に壁があるか、地面があるか、直線上の最初の対象を知りたいなら RayCast2D が向いています。

ノード向いている判定
Area2D円や四角の範囲に入ったもの
RayCast2D直線上にある最初のもの
ShapeCast2D形状を動かした先で当たるもの

敵の大まかな索敵範囲はArea2D、壁越しに見えているかの確認はRayCast2D、というように組み合わせることもあります。

まとめ

  • Area2D は、移動を止める壁ではなく、通り抜けるセンサー
  • 物理ボディを検出するなら body_entered / body_exited
  • Area2D同士を検出するなら area_entered / area_exited
  • 範囲内にいる間の処理は、get_overlapping_bodies() またはリスト管理で行う
  • 反応しないときは CollisionShape2D、Layer/Mask、monitoring を確認する

Area2Dを使いこなすと、ゲーム世界が「近づいたら反応する」「範囲に入ったら変化する」ようになります。アイテム、敵AI、攻撃判定、イベント開始など、かなり多くの仕組みの土台になるノードです。

さらに学ぶために