Godot 엔진에서 2D게임 몬스터 구현하기
지난 글에서 HP와 기력 바를 만들었는데, 이어서 플레이어와 상호작용할 몬스터를 구현해보려고 한다.
몬스터는 무작위 방향으로 일정 시간 동안 이동한 뒤, 쉬는 동작을 반복하도록 한다. 이를 위해 상태 머신(State Machine)을 사용해서 몬스터의 행동을 관리한다. Godot 4.4에서 CharacterBody2D를 기반으로 몬스터를 구현하고, 플레이어와의 충돌을 감지해서 데미지를 주도록 설정하겠다.
몬스터 씬 생성
새 씬 생성
![]() |
| 몬스터 씬 추가 |
먼저 몬스터를 재사용 가능하게 하기 위해 별도의 씬을 만든다. main 씬 옆에 '+' 버튼을 눌러 씬을 추가하고 'monster'라는 이름으로 저장했다.
몬스터 씬 노드 구성
![]() |
| 몬스터 씬 노드 구성 |
'ChracterBody2D' 노드를 루트 노드로 만들고 이름을 'Monster'로 바꾸어주었다. 하위 노드에 'Sprite2D' 노드를 만들어 이미지를 추가하고 'CollisionShape2D'를 만들어 다른 개체와 겹치지 않게 설정해준다. 'Area2D' 노드를 추가하고 그 하위에 'CollisionShape2D'를 또 추가해주었는데, 이는 플레이어가 이 영역에 진입할 시 피해를 입게 하도록 설정하기 위함이다.
몬스터 스프라이트 추가
![]() |
| 스프라이트 이미지 설정 |
'Sprite2D' 노드의 인스펙터에서 'Texture' 항목을 통해 이미지를 설정하면 된다. 임시로 색상을 채워넣기 위해 'CanvasTexture'를 선택하고 색상을 노란색으로 만들어주었다.
충돌 영역 추가
![]() |
| 충돌 영역 설정 |
충돌 영역도 인스펙터의 'Shape' 항목을 통해 모양을 지정해준 뒤 스프라이트 이미지에 맞춰 크기를 조절해주었다.
몬스터 스크립트 작성
![]() |
| 스크립트 붙이기 |
Monster 노드에 '스크립트 붙이기'를 통해 새 스크립트 추가해주고 아래와 같이 코드를 작성했다.
충돌 감지 설정
몬스터가 플레이어와 충돌하면 데미지를 주려면 몇 가지 설정이 필요하다.
플레이어 그룹 설정
![]() |
| 플레이어 그룹 설정 |
Player 노드 선택하고 인스펙터 옆을 보면 '노드' 탭이 있다. 그 아래 '시그널'/'그룹' 섹션을 선택할 수 있는데 '그룹'을 선택한다. '+' 버튼을 눌러 그룹을 추가하고 이름을 'player'로 지어주었다. 이제 Player 노드가 'player'라는 그룹에 속하게 되었다.
충돌 신호 연결
Scene 패널에서 'Area2D' 노드 선택하고 인스펙터 옆 '노드' 탭을 선택한다. '시그널'/'그룹' 에서 시그널 섹션을 선택하고, 'body_entered(body: Node)'라는 신호를 찾는다.
신호를 우클릭하면 '연결'을 찾을 수 있다.
![]() |
| 충돌 신호 연결 |
받는 메서드가 '_on_area_2d_body_entered'로 되어 있는데, 기본 세팅 그대로 연결해주면 된다.
Collision 레이어 설정
![]() |
| Area2D 노드의 Collision 레이어 설정 |
![]() |
| Player 노드의 Collision 레이어 설정 |
'CollisionShpe2D' 노드가 여러 개라 그런지 몬스터 자신의 CollsionShape2D 노드와 충돌하는 문제가 발생해서 Area2D 노드와 Player 노드의 Collision 레이어를 다음과 같이 수정해주었다.
Collision Layer: 1 (기본 레이어, 물리 충돌용).
Monster의 Area2D:
Collision Layer: 0 (Area2D는 물리 충돌에 관여하지 않으므로 비활성화).
Collision Mask: 2 (플레이어가 속한 레이어로 설정).
Collision Layer: 2 (Area2D가 감지할 레이어).
Collision Mask: 1 (Ground, Platform 등과 충돌).
Area2D는 레이어 2(플레이어)만 감지하므로, 몬스터 자신(레이어 1)과의 충돌을 무시하는 결과를 보여준다.
플레이어 스크립트 수정
몬스터에게 피해를 입을 때 넉백 효과를 주기 위해 플레이어 스크립트 또한 다음과 같이 수정했다.
# 현실 물리 단위 (1m = 100px로 가정)
const METERS_TO_PIXELS = 100.0 # 1미터 = 100픽셀
# 물리 상수
@export var max_speed: float = 2.0 * METERS_TO_PIXELS # 최대 속도 (2 m/s → 200 px/s)
@export var ground_acceleration: float = 10.0 * METERS_TO_PIXELS # 가속도 (10 m/s² → 1000 px/s²)
@export var ground_friction: float = 8.0 * METERS_TO_PIXELS # 마찰력 (8 m/s² → 800 px/s²)
@export var air_drag: float = 0.02 # 공기 저항 계수
@export var gravity: float = 9.8 * METERS_TO_PIXELS # 중력 (9.8 m/s² → 980 px/s²)
@export var jump_strength: float = -4.0 * METERS_TO_PIXELS # 점프 힘 (-4 m/s → -400 px/s)
@export var max_fall_speed: float = 20.0 * METERS_TO_PIXELS # 최대 낙하 속도 (20 m/s → 2000 px/s)
# 체력과 기력
@export var max_hp: float = 100.0
@export var max_stamina: float = 100.0
var hp: float = max_hp
var stamina: float = max_stamina
@export var stamina_recovery_rate: float = 5.0
# Knockback 속성
var is_knocked_back: bool = false
@export var knockback_strength: float = 300.0
@export var knockback_duration: float = 0.3
# 데미지 쿨다운 속성
var can_take_damage: bool = true
@export var damage_cooldown: float = 1.0
# UI 참조
@onready var hp_bar = get_node("/root/Main/UI/HPBar")
@onready var stamina_bar = get_node("/root/Main/UI/StaminaBar")
var last_hp: float = hp
var last_stamina: float = stamina
var input_direction: float = 0.0
func _ready() -> void:
hp_bar.max_value = max_hp
hp_bar.value = hp
stamina_bar.max_value = max_stamina
stamina_bar.value = stamina
func _physics_process(delta: float) -> void:
# Knockback 중일 때는 입력과 수평 이동 계산 무시
if is_knocked_back:
# 중력만 적용
if not is_on_floor():
velocity.y += gravity * delta
if velocity.y > max_fall_speed:
velocity.y = max_fall_speed
move_and_slide()
return
# 입력 방향 계산
input_direction = 0.0
if Input.is_action_pressed("ui_right"):
input_direction += 1.0
if Input.is_action_pressed("ui_left"):
input_direction -= 1.0
# 수평 이동
if is_on_floor():
if input_direction != 0:
velocity.x += input_direction * ground_acceleration * delta
else:
var friction_force: float = -sign(velocity.x) * ground_friction * delta
if abs(velocity.x) > abs(friction_force):
velocity.x += friction_force
else:
velocity.x = 0.0
else:
if input_direction != 0:
velocity.x += input_direction * ground_acceleration * 0.5 * delta
velocity.x -= velocity.x * air_drag
# 최대 속도 제한
velocity.x = clamp(velocity.x, -max_speed, max_speed)
# 점프
if Input.is_action_just_pressed("ui_select") and is_on_floor():
velocity.y = jump_strength
# 중력 적용
if not is_on_floor():
velocity.y += gravity * delta
# 최대 낙하 속도 제한
if velocity.y > max_fall_speed:
velocity.y = max_fall_speed
# 플레이어 상태 관리
update_player_state(delta)
# 디버깅 출력
print("Velocity X: ", velocity.x, " Velocity Y: ", velocity.y, " HP: ", hp, " Stamina: ", stamina)
# 이동 적용
move_and_slide()
func update_player_state(delta: float) -> void:
# 기력 회복
if is_on_floor():
stamina += stamina_recovery_rate * delta
stamina = clamp(stamina, 0, max_stamina)
# UI 업데이트
update_ui()
# UI 업데이트 함수
func update_ui() -> void:
if last_hp != hp or last_stamina != stamina:
hp_bar.value = hp
stamina_bar.value = stamina
last_hp = hp
last_stamina = stamina
# 데미지 받기 함수
func take_damage(amount: float, source: Node = null) -> void:
if not can_take_damage:
print("Player on damage cooldown, ignoring damage.")
return
print("Player taking damage: ", amount)
hp -= amount
hp = clamp(hp, 0, max_hp)
if hp <= 0:
print("Player died!")
can_take_damage = false
apply_knockback(source)
await get_tree().create_timer(damage_cooldown).timeout
can_take_damage = true
# Knockback 적용 함수
func apply_knockback(source: Node = null) -> void:
if is_knocked_back:
print("Already in knockback, skipping.")
return
is_knocked_back = true
# source가 없으면 기본 방향으로 Knockback
var direction = Vector2(1, 0)
if source:
direction = (global_position - source.global_position).normalized()
print("Knockback source: ", source.name, " Direction: ", direction)
else:
print("No source provided, using default direction: ", direction)
velocity = direction * knockback_strength
velocity.y = -200.0
print("Knockback velocity applied: ", velocity)
await get_tree().create_timer(knockback_duration).timeout
is_knocked_back = false
print("Knockback ended.")
![]() |
| 테스트 플레이 화면 |
테스트 결과 몬스터가 좌우로 움직이고, 플레이어가 부딪히면 HP가 감소하며 넉백 효과가 나타난다. 다만 몬스터가 이동 중일 때, 정면에서 부딪힐 때만 피해 효과가 나타나는 문제가 있다. 스크립트를 어떻게 수정할지 조금 더 고민해봐야겠다.











댓글
댓글 쓰기