본문 바로가기
Unity 궁둥이 쪼물쪼물

010. 플레이어 체력 & 게임오버

by daco2020 2026. 4. 13.

플레이어에게 체력을 부여하고, 적의 공격에 데미지를 받고,
HP가 0이 되면 버드아이뷰 카메라 연출과 함께 게임오버되는 기능을 구현한 과정을 기록한다.

 

 

 

1. PlayerHealth.cs — 새로 만든 스크립트

역할

플레이어 HP 관리 + 데미지 처리 + 게임오버 연출

핵심 변수

[SerializeField] private int maxHp = 5;
[SerializeField] private float invincibleDuration = 1f;
[SerializeField] private Text hpText;
[SerializeField] private GameObject gameOverPanel;
[SerializeField] private float birdEyeHeight = 30f;
[SerializeField] private float cameraTransitionSpeed = 2f;

 

 

 

2. 데미지 처리: TakeDamage()

public void TakeDamage(int damage = 1)
{
    if (_isDead || _isInvincible) return;

    _hp -= damage;
    UpdateUI();

    if (_hp <= 0)
    {
        _isDead = true;
        GameOver();
    }
    else
    {
        StartCoroutine(InvincibleTimer());
    }
}

 

무적 시간이 필요한 이유

무적 시간이 없으면 적의 공격 한 번에 여러 프레임 동안 데미지가 중복으로 들어갈 수 있다.
1초간 무적 상태를 유지해서 한 번에 1 데미지만 받도록 보호한다.

맞음 → HP 감소 → 1초 무적 → 무적 해제 → 다시 맞을 수 있음
          이 동안은 맞아도 무시!

 

 

 

3. 적의 공격이 데미지를 주는 구조

역할 분리

Enemy: "공격 범위 안에 있으면 데미지를 요청한다"
Player: "데미지를 받으면 HP를 깎고 처리한다"
// Enemy.cs
public void DealDamageToPlayer()
{
    if (_playerTransform == null) return;
    if (!IsPlayerInRange(attackRange)) return;  // 범위 밖이면 무시!

    PlayerHealth playerHealth = _playerTransform.GetComponent<PlayerHealth>();
    if (playerHealth != null)
        playerHealth.TakeDamage();
}

 

범위 체크가 중요한 이유

처음에는 범위 체크 없이 구현했더니, 적이 점프(공격 모션)만 하면 플레이어가 피해도 데미지가 들어갔다. IsPlayerInRange(attackRange) 한 줄 추가로 해결!

 

InstantiateFx()에서 데미지 호출

적의 공격 애니메이션에 이미 InstantiateFx()가 Animation Event로 연결되어 있었다. 여기에 DealDamageToPlayer()를 추가해서, 이펙트가 터지는 타이밍에 데미지를 준다.

public void InstantiateFx()
{
    Instantiate(splashFx, transform.position, Quaternion.identity);
    DealDamageToPlayer();  // 이펙트 + 데미지 동시에
}

 

 

 

4. 게임오버 연출: 버드아이뷰 카메라

처음 구현: Time.timeScale = 0

처음에는 게임오버 시 Time.timeScale = 0으로 게임을 완전히 멈췄다. 하지만 게임이 계속 동작시키면서 게임 전경을 보여주고 싶었다. 나는 적이 얼마나 플레이어를 압도적으로 공격하는지 보여주고 싶었기 때문이다.

 

최종 구현: 카메라 전환 + 조작 차단

private void GameOver()
{
    // 1. UI 표시
    gameOverPanel.SetActive(true);

    // 2. 조작 차단 (이동, 발사, 카메라 회전 모두 비활성화)
    GetComponent<MoveControl>().enabled = false;
    GetComponent<PlayerShooting>().enabled = false;
    CameraControl camControl = _mainCamera.GetComponent<CameraControl>();
    if (camControl != null)
        camControl.enabled = false;

    // 3. 카메라를 Player에서 분리
    _mainCamera.SetParent(null);

    // 4. 버드아이뷰 목표 위치/회전 설정
    _targetCameraPos = transform.position
        + new Vector3(-birdEyeHeight * 1f, birdEyeHeight, -birdEyeHeight * 1f);
    _targetCameraRot = Quaternion.Euler(45f, 45f, 0f);
    _isCameraTransitioning = true;

    // 5. 마우스 커서 표시
    Cursor.visible = true;
    Cursor.lockState = CursorLockMode.None;
}

카메라 전환 원리

게임오버 전:
  카메라 = Player의 자식 → Player가 움직이면 같이 움직임

게임오버 후:
  카메라 = Player에서 분리 (SetParent(null))
  → 독립적으로 하늘 위 대각선 위치로 이동
  → 45도 각도로 전경을 내려다봄

Lerp — 부드러운 카메라 이동

private void Update()
{
    if (!_isCameraTransitioning) return;

    _mainCamera.position = Vector3.Lerp(
        _mainCamera.position,    // 현재 위치
        _targetCameraPos,        // 목표 위치
        Time.deltaTime * cameraTransitionSpeed  // 보간 속도
    );
    _mainCamera.rotation = Quaternion.Lerp(
        _mainCamera.rotation,    // 현재 회전
        _targetCameraRot,        // 목표 회전
        Time.deltaTime * cameraTransitionSpeed
    );
}

 

Lerp(Linear Interpolation, 선형 보간)이란?
두 값 사이를 부드럽게 이어주는 함수.

Lerp(A, B, 0.0) = A          (시작점)
Lerp(A, B, 0.5) = A와 B 중간
Lerp(A, B, 1.0) = B          (끝점)

 

매 프레임 현재 위치에서 목표까지 조금씩 이동하니까, 처음에 빠르다가 점점 느려지는 자연스러운 감속 효과가 난다.\

 

 

카메라 위치 계산

플레이어 위치 + (-30, 30, -30)

     카메라 📷
      ╲  (높이 30, 대각선 뒤)
       ╲
        ╲  45도
         ╲
     플레이어 🧍

 

Inspector에서 birdEyeHeight 값을 조절하면 높이와 거리가 같이 변한다.

 

 

 

5. UI 구성

Hierarchy 구조

Canvas
  ├── Aim (크로스헤어, 기존)
  ├── HPText              ← 새로 추가
  └── GameOverPanel       ← 새로 추가 (기본 비활성화)
        └── GameOverText

HPText

  • 위치: 왼쪽 위
  • 내용: HP: 5 / 5 (코드에서 자동 업데이트)

GameOverPanel

  • 반투명 검정 배경 (A: 150)
  • 가운데에 큰 텍스트
  • 시작 시 비활성화 (Inspector에서 체크 해제) → 게임오버 시 코드에서 활성화

6. enabled = false로 조작 차단

게임오버 후에도 WASD, 마우스, 클릭이 먹히면 안 된다. 스크립트의 enabled를 false로 바꾸면 Update()가 호출되지 않아서 조작이 완전히 차단된다.

GetComponent<MoveControl>().enabled = false;      // 이동 차단
GetComponent<PlayerShooting>().enabled = false;    // 발사 차단
camControl.enabled = false;                        // 카메라 회전 차단

 

Time.timeScale = 0과의 차이:

  • timeScale = 0: 게임 전체가 멈춤 (적도 멈춤)
  • enabled = false: 특정 스크립트만 멈춤 (적은 계속 돌아다님)

 

 

 

7. 이번 STEP에서 배운 핵심 정리

  1. TakeDamage() 패턴 = OnHit()과 동일한 역할 분리 구조
  2. 무적 시간 = 코루틴으로 중복 데미지 방지
  3. IsPlayerInRange() = 공격 범위 체크로 피할 수 있게
  4. Lerp = 부드러운 보간 (카메라 이동에 활용)
  5. SetParent(null) = 자식 오브젝트를 부모에서 분리
  6. enabled = false = Update() 비활성화 (조작 차단)
  7. Time.timeScale = 0 vs enabled = false 차이 이해
  8. UI 패널은 시작 시 비활성화, 이벤트 시 활성화