feat: 게임 오버 및 승리 UI 구현

- GameOverUI: 게임 오버 화면 표시
- VictoryUI: 보스 처치 시 승리 화면 표시
- VictoryEffect: 슬로우 모션, 카메라 연출, 이펙트 재생

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-03-14 15:08:34 +09:00
parent 0a1aeea825
commit 00233ee977
7 changed files with 315 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9598f5b3fb42a1945ab57c2dc55b2815
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,187 @@
using System.Collections;
using UnityEngine;
using Colosseum.Enemy;
namespace Colosseum.Effects
{
/// <summary>
/// 보스 승리 연출 이펙트.
/// 보스 사망 시 카메라 연출, 이펙트, 슬로우 모션 등을 처리합니다.
/// GameManager에 의해 활성화됩니다.
/// </summary>
public class VictoryEffect : MonoBehaviour
{
[Header("Victory Settings")]
[Tooltip("승리 시 슬로우 모션 배율")]
[SerializeField] private float slowMotionScale = 0.3f;
[Tooltip("슬로우 모션 지속 시간")]
[SerializeField] private float slowMotionDuration = 2f;
[Header("Effects")]
[Tooltip("승리 시 생성할 이펙트 프리팹")]
[SerializeField] private GameObject victoryEffectPrefab;
[Tooltip("이펙트 생성 위치 오프셋")]
[SerializeField] private Vector3 effectOffset = Vector3.up * 2f;
[Header("Audio")]
[Tooltip("승리 사운드")]
[SerializeField] private AudioClip victorySound;
[Tooltip("사운드 볼륨")]
[SerializeField] private float soundVolume = 1f;
[Header("Debug")]
[SerializeField] private bool debugMode = true;
// 상태
private bool isPlaying = false;
private float originalTimeScale;
private Camera mainCamera;
private void Awake()
{
mainCamera = Camera.main;
}
private void OnEnable()
{
PlayVictoryEffect();
}
private void OnDisable()
{
// 시간 스케일 복구
if (isPlaying)
{
Time.timeScale = 1f;
isPlaying = false;
}
}
/// <summary>
/// 승리 연출 재생
/// </summary>
public void PlayVictoryEffect()
{
if (isPlaying) return;
StartCoroutine(VictorySequence());
}
private IEnumerator VictorySequence()
{
isPlaying = true;
originalTimeScale = Time.timeScale;
if (debugMode)
{
Debug.Log("[VictoryEffect] Starting victory sequence");
}
// 1. 슬로우 모션
yield return StartCoroutine(PlaySlowMotion());
// 2. 카메라 연출
yield return StartCoroutine(PlayCameraEffect());
// 3. 이펙트 생성
SpawnVictoryEffect();
// 4. 사운드 재생
PlayVictorySound();
// 5. 시간 복구
Time.timeScale = originalTimeScale;
isPlaying = false;
if (debugMode)
{
Debug.Log("[VictoryEffect] Victory sequence complete");
}
}
private IEnumerator PlaySlowMotion()
{
float elapsed = 0f;
while (elapsed < slowMotionDuration)
{
elapsed += Time.unscaledDeltaTime;
float t = elapsed / slowMotionDuration;
// 처음에는 슬로우, 나중에는 복구
if (t < 0.5f)
{
Time.timeScale = Mathf.Lerp(originalTimeScale, slowMotionScale, t * 2f);
}
else
{
Time.timeScale = Mathf.Lerp(slowMotionScale, originalTimeScale, (t - 0.5f) * 2f);
}
yield return null;
}
Time.timeScale = slowMotionScale;
}
private IEnumerator PlayCameraEffect()
{
if (mainCamera == null || BossEnemy.ActiveBoss == null)
yield break;
// 보스 위치로 카메라 이동
Transform bossTransform = BossEnemy.ActiveBoss.transform;
Vector3 targetPosition = bossTransform.position + effectOffset;
float elapsed = 0f;
float duration = slowMotionDuration * 0.5f;
while (elapsed < duration)
{
elapsed += Time.unscaledDeltaTime;
// 카메라가 보스를 바라보도록
mainCamera.transform.LookAt(targetPosition);
yield return null;
}
}
private void SpawnVictoryEffect()
{
if (victoryEffectPrefab == null) return;
Vector3 spawnPosition = transform.position + effectOffset;
if (BossEnemy.ActiveBoss != null)
{
spawnPosition = BossEnemy.ActiveBoss.transform.position + effectOffset;
}
var effect = Instantiate(victoryEffectPrefab, spawnPosition, Quaternion.identity);
// 일정 시간 후 제거
Destroy(effect, 5f);
if (debugMode)
{
Debug.Log("[VictoryEffect] Spawned victory effect");
}
}
private void PlayVictorySound()
{
if (victorySound == null) return;
AudioSource.PlayClipAtPoint(victorySound, transform.position, soundVolume);
if (debugMode)
{
Debug.Log("[VictoryEffect] Played victory sound");
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b7cbb78d0cea7014ba69a25271583954

View File

@@ -0,0 +1,59 @@
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace Colosseum.UI
{
/// <summary>
/// 게임 오버 UI 컨트롤러.
/// GameManager에 의해 활성화/비활성화됩니다.
/// </summary>
public class GameOverUI : MonoBehaviour
{
[Header("UI References")]
[Tooltip("게임 오버 텍스트")]
[SerializeField] private TMP_Text gameOverText;
[Tooltip("재시작 카운트다운 텍스트")]
[SerializeField] private TMP_Text countdownText;
[Tooltip("게임 오버 애니메이터")]
[SerializeField] private Animator animator;
[Header("Settings")]
[Tooltip("게임 오버 텍스트")]
[SerializeField] private string gameOverMessage = "GAME OVER";
[Tooltip("텍스트 색상")]
[SerializeField] private Color textColor = Color.red;
private void Start()
{
if (gameOverText != null)
{
gameOverText.text = gameOverMessage;
gameOverText.color = textColor;
}
}
private void OnEnable()
{
// 애니메이션 재생
if (animator != null)
{
animator.SetTrigger("Show");
}
}
/// <summary>
/// 카운트다운 텍스트 업데이트
/// </summary>
public void UpdateCountdown(int seconds)
{
if (countdownText != null)
{
countdownText.text = $"Restarting in {seconds}...";
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f87daf25f7fc5c4499b66d327b6c4cf2

View File

@@ -0,0 +1,55 @@
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using Colosseum.Enemy;
namespace Colosseum.UI
{
/// <summary>
/// 승리 UI 컨트롤러.
/// GameManager에 의해 활성화/비활성화됩니다.
/// </summary>
public class VictoryUI : MonoBehaviour
{
[Header("UI References")]
[Tooltip("승리 텍스트")]
[SerializeField] private TMP_Text victoryText;
[Tooltip("보스 이름 텍스트")]
[SerializeField] private TMP_Text bossNameText;
[Tooltip("승리 애니메이터")]
[SerializeField] private Animator animator;
[Header("Settings")]
[Tooltip("승리 텍스트")]
[SerializeField] private string victoryMessage = "VICTORY!";
[Tooltip("텍스트 색상")]
[SerializeField] private Color textColor = Color.yellow;
private void Start()
{
if (victoryText != null)
{
victoryText.text = victoryMessage;
victoryText.color = textColor;
}
}
private void OnEnable()
{
// 보스 이름 표시
if (bossNameText != null && BossEnemy.ActiveBoss != null)
{
bossNameText.text = $"{BossEnemy.ActiveBoss.name} Defeated!";
}
// 애니메이션 재생
if (animator != null)
{
animator.SetTrigger("Show");
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 514ff17abf102744faf81dbad1251d86