플레이어에게 체력을 부여하고, 적의 공격에 데미지를 받고,
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에서 배운 핵심 정리
- TakeDamage() 패턴 = OnHit()과 동일한 역할 분리 구조
- 무적 시간 = 코루틴으로 중복 데미지 방지
- IsPlayerInRange() = 공격 범위 체크로 피할 수 있게
- Lerp = 부드러운 보간 (카메라 이동에 활용)
- SetParent(null) = 자식 오브젝트를 부모에서 분리
- enabled = false = Update() 비활성화 (조작 차단)
- Time.timeScale = 0 vs enabled = false 차이 이해
- UI 패널은 시작 시 비활성화, 이벤트 시 활성화
'Unity 궁둥이 쪼물쪼물' 카테고리의 다른 글
| 011. 싱글톤 GameManager & 승리 조건 (0) | 2026.04.15 |
|---|---|
| 009. 적 체력(HP) 시스템 (0) | 2026.04.13 |
| 008. 넉백 코드 리팩토링 (0) | 2026.04.13 |
| 007. 넉백(Knockback) 구현 (0) | 2026.04.06 |
| 006. 총알에 오브젝트 풀링 적용하기 (0) | 2026.04.06 |