오늘 구현한 것

  • 좀비-식물 충돌 (Area2D overlap 방식)
  • 좀비 멈춤 + 주기적 공격
  • 식물 HP 및 피격 flash
  • 식물 사망 시 좀비 재개
  • 게임오버 조건 및 화면
  • 오프닝 화면 + 게임 시작 버튼

Collision Layer / Mask 설계

Area2D 기반 충돌이므로 Layer/Mask를 명확히 설계해야 했다.

오브젝트LayerMask
Plant1-
Zombie21
Pea32
Sun4-
  • 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 베이스 씬이 없어서 에디터 연결이 불가능했기 때문이다.

GDScript
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 구현

GDScript
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 플래그를 사용했다.

GDScript
# 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 애니메이션을 적용했다.

GDScript
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 플래그로 막았다.

GDScript
# _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 플래그로 한 번만 실행되도록 처리했다.