오늘 구현한 것
- 좀비-식물 충돌 (Area2D overlap 방식)
- 좀비 멈춤 + 주기적 공격
- 식물 HP 및 피격 flash
- 식물 사망 시 좀비 재개
- 게임오버 조건 및 화면
- 오프닝 화면 + 게임 시작 버튼
Collision Layer / Mask 설계
Area2D 기반 충돌이므로 Layer/Mask를 명확히 설계해야 했다.
| 오브젝트 | Layer | Mask |
|---|---|---|
| Plant | 1 | - |
| Zombie | 2 | 1 |
| Pea | 3 | 2 |
| Sun | 4 | - |
- Zombie(Mask=1)가 Plant(Layer=1)만 감지
- Pea(Mask=2)가 Zombie(Layer=2)만 감지
- Sun을 Layer=4로 설정하지 않으면 Zombie가 Sun을 Plant로 착각해
died시그널 접근 시 에러 발생
Plant 베이스 씬에 CollisionShape2D가 없었으므로 추가했다. 베이스에 추가하면 Peashooter, Sunflower 모두 자동 적용된다.
좀비-식물 충돌 구현
zombie.gd에서 area_entered 시그널을 코드로 연결했다. Zombie 베이스 씬이 없어서 에디터 연결이 불가능했기 때문이다.
func _ready():
area_entered.connect(_on_area_entered)
func _on_area_entered(plant):
speed = 0.0
target_plant = plant
target_plant.died.connect(_on_plant_died)
attack_timer.start()
func _on_plant_died(cell):
speed = 50
attack_timer.stop()
좀비는 attack_timer (0.5초 주기)로 target_plant.take_damage(ad)를 반복 호출한다.
Plant 베이스 클래스 take_damage 구현
func take_damage(amount: int) -> void:
health -= amount
modulate = Color(1.5, 1.5, 1.5)
damage_timer.start()
if health <= 0:
died.emit(cell)
피격 시 밝아졌다가 0.18초 후 복귀하는 flash 효과를 넣었다.
게임오버 구현
좀비가 집에 도달하면 reached_house 시그널 → GameBoard → Main으로 relay하는 구조다.
중복 호출 방지를 위해 is_game_over 플래그를 사용했다.
# game_board.gd
var is_game_over = false
func _on_zombie_reached_house(zombie, row):
if is_game_over:
return
is_game_over = true
game_over.emit()
게임오버 이미지는 Tween으로 scale 0 → 1 애니메이션을 적용했다.
func _on_game_board_game_over():
bgm.stop()
endbgm.play()
sun_timer.stop()
game_over_image.visible = true
game_over_image.scale = Vector2(0, 0)
var tween = create_tween()
tween.tween_property(game_over_image, "scale", Vector2(1, 1), 0.5)
게임오버 이미지가 PlantUI 위에 나와야 하므로 별도 CanvasLayer(layer=2)에 넣었다.
오프닝 구현
오프닝 CanvasLayer가 위에 있으면 마우스 입력을 자연스럽게 막아준다. 키보드 입력은 game_started 플래그로 막았다.
# _on_start_button_pressed
func _on_start_button_pressed():
bgm.play()
main_image.visible = false
sun_timer.start()
$GameBoard.game_started = true
SunTimer의 Autostart를 꺼서 오프닝 중 해가 떨어지지 않도록 했다.
겪은 문제들
super._ready() 누락
Plant를 상속받는 Peashooter, Sunflower에서 super._ready()를 호출하지 않으면 부모의 _ready()가 실행되지 않는다. Java는 부모 생성자를 암묵적으로 호출하지만 GDScript(Python 계열)는 명시적으로 호출해야 한다. Sunflower에서 super._ready() 누락으로 damage_timer가 null이었고, take_damage 호출 시 에러가 발생했다.
타이머 콜백 이름 충돌
Plant 베이스의 damage_timer가 _on_timer_timeout에 연결되어 있었고, Peashooter도 동일한 이름의 _on_timer_timeout을 사용했다. super._ready() 호출 시 Plant의 타이머가 Peashooter의 발사 함수에 연결되어 피격받을 때마다 콩을 발사하는 버그가 발생했다. Plant의 콜백을 _on_damage_timer_timeout으로 변경해 해결했다.
시그널 인자 수 불일치
died(cell) 시그널을 _on_plant_died()로 받으면 GDScript는 허용하지만, 실제로는 버전에 따라 에러가 발생할 수 있다. _on_plant_died(cell)로 인자를 받아주는 게 안전하다.
Sun Layer 미설정
Sun이 Layer=1(기본값)인 상태에서 Zombie(Mask=1)가 Sun을 감지했다. Sun에는 died 시그널이 없으므로 접근 시 에러가 발생했다. Sun의 Layer를 4로 변경해 해결했다.
area_entered 중복 연결
Zombie 베이스 스크립트(_ready에서 connect)와 BasicZombie(super._ready() 호출)가 맞물려 area_entered가 이미 연결된 시그널에 중복 연결되는 에러가 발생했다. 에디터 Node 탭에서 중복 연결을 제거해 해결했다.
게임오버 중복 호출
여러 좀비가 거의 동시에 집에 도달하면 game_over가 여러 번 emit되어 게임오버 화면이 중복으로 생성됐다. is_game_over 플래그로 한 번만 실행되도록 처리했다.
