충돌 레이어가 중요한 이유
충돌 레이어는 Godot 4의 모든 물리 상호작용(이동, 히트 감지, 레이캐스팅, Area 트리거 등)의 근간입니다. Layer와 Mask의 차이를 이해하는 것은 매우 중요하며, 이를 오해하는 것은 초보자와 숙련된 개발자 모두에게 가장 흔한 골칫거리 중 하나입니다.
Layer vs Mask — 개념 모델
Godot의 모든 물리 객체는 두 개의 비트마스크 속성을 가집니다:
- Layer = "나는 이 레이어에 존재한다"(내가 무엇인지)
- Mask = "나는 이 레이어를 스캔한다"(내가 보거나 맞힐 수 있는 대상)
핵심 규칙
A의 Mask가 B의 Layer를 포함하거나 또는 B의 Mask가 A의 Layer를 포함할 때 A와 B가 충돌합니다. 충돌이 발생하려면 한쪽만 상대를 "볼" 수 있으면 됩니다.
프로젝트 설정에서 레이어에 이름 붙이기
코드를 작성하기 전에 먼저 레이어에 이름을 붙이세요. 인스펙터를 훨씬 쉽게 사용할 수 있게 해주고, 프로젝트가 커져도 혼란을 방지합니다.
프로젝트 설정 > 레이어 이름 > 2D 물리(또는 3D 물리):
| 레이어 번호 | 이름 | 용도 |
|---|---|---|
| 1 | Player | 플레이어 캐릭터 |
| 2 | Enemy | 모든 적 바디 |
| 3 | Environment | 벽, 바닥, 플랫폼 |
| 4 | Projectile | 총알, 화살, 마법 |
| 5 | Pickup | 아이템, 코인, 회복 팩 |
| 6 | Trigger | 센서, 스폰 존, 체크포인트 |
코드에서 레이어 설정하기 (GDScript)
Godot 4는 두 가지 방식을 제공합니다 — 값 기반 API(권장)와 비트마스크 직접 대입:
# Godot 4 API — value-based (1-indexed, recommended)
set_collision_layer_value(1, true) # I am on layer 1
set_collision_mask_value(2, true) # I detect layer 2
# Or use bitmask directly
collision_layer = 1 # Layer 1 only (bit 0)
collision_mask = 6 # Layers 2 and 3 (bits 1 + 2 = 6)
# Read current state
var on_layer_1: bool = get_collision_layer_value(1)
var scans_layer_2: bool = get_collision_mask_value(2)
비트마스크 함정
set_collision_layer_value()의 레이어 번호는 1부터 시작하지만, 내부 비트마스크는 0부터 시작합니다. 레이어 1 = 비트 0 = 값 1, 레이어 2 = 비트 1 = 값 2, 레이어 3 = 비트 2 = 값 4. 헷갈릴 때는 실수를 피하기 위해 값 기반 API를 사용하세요.
흔한 레이어 구성 예
플랫포머 / 사이드 스크롤
| 객체 | Layer | Mask | 설명 |
|---|---|---|---|
| Player | 1 | 2, 3, 5, 6 | 적, 환경, 아이템, 트리거를 감지 |
| Enemy | 2 | 1, 3 | 플레이어와 환경을 감지 |
| Environment | 3 | — | 수동적 — 다른 객체에게 감지됨 |
| Projectile | 4 | 2, 3 | 적과 벽에 맞음 |
| Pickup | 5 | — | 수동적 — 플레이어가 감지 |
| Trigger | 6 | 1 | 플레이어만 감지 |
탑다운 / 로그라이크
| 객체 | Layer | Mask | 설명 |
|---|---|---|---|
| Player | 1 | 2, 3, 5, 6 | 적, 벽, NPC, 센서를 감지 |
| Enemy | 2 | 1, 3 | 플레이어와 벽을 감지 |
| Wall | 3 | — | 수동적 |
| Bullet | 4 | 1, 2, 3 | 플레이어, 적, 벽에 맞음 |
| NPC | 5 | 3 | 벽하고만 충돌 |
| Sensor | 6 | 1 | 플레이어의 진입을 감지 |
Area2D / Area3D 레이어
Area도 동일한 Layer/Mask 시스템을 사용합니다. 추가로 두 개의 토글 속성이 있습니다:
-
monitoring—true이면, 이 Area가 진입하는 다른 객체를 능동적으로 감지합니다. -
monitorable—true이면, 다른 Area가 이 Area를 감지할 수 있습니다.
시그널: area_entered vs body_entered
body_entered는 PhysicsBody(CharacterBody, RigidBody, StaticBody)가 Area에 들어올 때 발생합니다. area_entered는 다른 Area가 들어올 때 발생합니다. 용도에 맞는 시그널을 연결하도록 주의하세요.
# Pickup Area2D — detect when the Player body enters
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("player"):
collect()
queue_free()
RayCast 충돌 마스크
RayCast 노드는 Mask만 가지며(Layer 없음), 물리 바디가 아닌 감지기이기 때문입니다. 레이가 맞힐 수 있는 대상을 필터링하려면 마스크를 설정하세요:
# RayCast that only detects enemies (layer 2)
$RayCast2D.collision_mask = 2 # Layer 2 = Enemy
# Or use the value-based API:
$RayCast2D.set_collision_mask_value(1, false) # Ignore Player
$RayCast2D.set_collision_mask_value(2, true) # Detect Enemy
$RayCast2D.set_collision_mask_value(3, false) # Ignore Environment
# Line-of-sight ray that ignores projectiles
$LineOfSight.collision_mask = 0
$LineOfSight.set_collision_mask_value(1, true) # Player
$LineOfSight.set_collision_mask_value(3, true) # Environment
실전 예제: Player – Enemy – Projectile
전형적인 액션 게임을 위한 완전한 레이어 설정입니다. 주석이 각 선택의 이유를 보여줍니다:
# Player (CharacterBody2D)
# Layer: 1 (Player) — "I am the player"
# Mask: 2 (Enemy), 3 (Environment), 5 (Pickup)
# — I collide with enemies, walls, and can pick up items
# Enemy (CharacterBody2D)
# Layer: 2 (Enemy) — "I am an enemy"
# Mask: 1 (Player), 3 (Environment)
# — I collide with the player and walls
# Player Bullet (Area2D)
# Layer: 4 (Projectile) — "I am a projectile"
# Mask: 2 (Enemy), 3 (Environment)
# — I hit enemies and walls, but NOT the player who fired me
# Enemy Bullet (Area2D)
# Layer: 4 (Projectile) — "I am a projectile"
# Mask: 1 (Player), 3 (Environment)
# — I hit the player and walls, but NOT the enemy who fired me
팁: 아군 오사 방지
Player Bullet은 레이어 2(Enemy)를 마스크하지만 레이어 1(Player)은 마스크하지 않고, Enemy Bullet은 레이어 1(Player)을 마스크하지만 레이어 2(Enemy)는 마스크하지 않는다는 점에 주목하세요. 이것이 순전히 레이어 구성만으로 아군 오사를 방지하는 방법입니다 — 코드가 필요 없습니다.
디버깅 팁
- 충돌 형태 표시: 에디터에서 디버그 > 충돌 형태 표시로 이동하면 실행 시 모든 충돌 형태가 렌더링됩니다. 누락되거나 어긋난 콜라이더를 즉시 발견할 수 있습니다.
- 인스펙터 확인: 노드를 선택하고 인스펙터에서 Collision > Layer와 Collision > Mask를 펼치세요. 각 비트에 마우스를 올리면 이름이 표시됩니다(프로젝트 설정에서 이름을 붙였다면).
-
실행 중 출력:
print("Layer: ", collision_layer, " Mask: ", collision_mask)로 게임 플레이 중 비트마스크 값을 검증하세요.
Godot 3 → 4 마이그레이션 변경 사항
| Godot 3 | Godot 4 |
|---|---|
set_collision_layer_bit(bit, value) |
set_collision_layer_value(layer, value) |
set_collision_mask_bit(bit, value) |
set_collision_mask_value(layer, value) |
bit 매개변수는 0부터 시작
|
layer 매개변수는 1부터 시작
|
| 20개 레이어 사용 가능 | 32개 레이어 사용 가능 |
마이그레이션 함정
Godot 3 프로젝트를 이식한다면, set_collision_layer_bit(0, true)가 set_collision_layer_value(1, true)로 바뀐다는 점을 기억하세요. 인덱스가 +1 이동합니다. 이를 놓치면 모든 레이어가 하나씩 어긋납니다.
흔한 실수
1. Mask 설정을 잊음
객체에 Layer는 있지만 Mask가 비어 있습니다(모두 0). 객체는 물리 세계에 존재하지만 아무것도 감지하지 못합니다. 마스크가 일치하는 다른 객체는 여전히 이 객체를 감지하지만, 이 객체의 move_and_slide()는 모든 것을 통과합니다.
2. Layer와 Mask를 혼동함
플레이어의 Mask 대신 Layer를 2(Enemy)로 설정합니다. 이제 물리 엔진 입장에서는 플레이어가 적이 되어 버립니다. 항상 기억하세요: Layer = 내가 무엇인지, Mask = 내가 무엇을 스캔하는지.
3. 레이어 번호 대신 비트마스크 값을 사용함
set_collision_layer_value(4, true)를 작성하면서 비트마스크 값 4(레이어 1+2)를 설정한다고 생각하지만, 실제로는 레이어 4를 활성화합니다. 값 기반 API는 비트 값이 아니라 레이어 번호를 받습니다.
4. 양방향을 기대했지만 단방향으로만 감지됨
객체 A는 B의 레이어를 마스크하지만 B는 A의 레이어를 마스크하지 않습니다. A의 move_and_slide()는 B와 충돌하지만, B의 move_and_slide()는 A를 통과합니다. 두 CharacterBody 노드가 서로를 막으려면 양쪽 모두 상대의 레이어를 자신의 마스크에 포함해야 합니다.
Godot MCP Pro로 물리 설정 자동화하기
레이어 체크박스를 수동으로 켜고 끄는 일은 그만하세요. AI가 레이어 명명, 마스크 할당, 레이캐스트 추가를 포함한 전체 충돌 설정을 몇 초 만에 구성해 줍니다.
Godot MCP Pro 받기 — $15setup_collision
set_physics_layers
get_physics_layers
get_collision_info
add_raycast