Files
Colosseum/Assets/Scripts/Enemy/BossEnemy.cs
dal4segno 5238b65dc2 feat: 적 사망 처리 시스템 개선
- EnemyBase: PlayDeathAnimationRpc로 모든 클라이언트에 사망 애니메이션 동기화
- EnemyAnimationController: 사망 상태에서 애니메이션 업데이트 및 트리거 중단
- BossEnemy: HandleDeath에서 AI 정지 순서 개선 (enabled=false 먼저)

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-16 09:46:56 +09:00

316 lines
9.8 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;
using Unity.Behavior;
using Colosseum.Stats;
namespace Colosseum.Enemy
{
/// <summary>
/// 보스 캐릭터. 페이즈 시스템과 동적 AI 전환을 지원합니다.
/// Unity Behavior 패키지를 사용하여 Behavior Tree 기반 AI를 구현합니다.
/// </summary>
public class BossEnemy : EnemyBase
{
[Header("Boss Settings")]
[Tooltip("보스 페이즈 데이터 목록 (순서대로 전환)")]
[SerializeField] private List<BossPhaseData> phases = new();
[Tooltip("초기 Behavior Graph")]
[SerializeField] private BehaviorGraph initialBehaviorGraph;
[Header("Phase Settings")]
[Tooltip("페이즈 전환 시 무적 시간")]
[Min(0f)] [SerializeField] private float phaseTransitionInvincibilityTime = 2f;
[Tooltip("페이즈 전환 연출 시간")]
[Min(0f)] [SerializeField] private float phaseTransitionDuration = 3f;
[Header("Debug")]
[SerializeField] private bool debugMode = true;
// 컴포넌트
private BehaviorGraphAgent behaviorAgent;
// 페이즈 상태
private int currentPhaseIndex = 0;
private bool isTransitioning = false;
private float phaseStartTime;
private float phaseElapsedTime;
private bool isInvincible = false;
// 커스텀 조건 딕셔너리
private Dictionary<string, bool> customConditions = new Dictionary<string, bool>();
// 이벤트
public event System.Action<int> OnPhaseChanged; // phaseIndex
public event System.Action<float> OnPhaseTransitionStart; // transitionDuration
public event System.Action OnPhaseTransitionEnd;
// 정적 이벤트 (UI 자동 연결용)
/// <summary>
/// 보스 스폰 시 발생하는 정적 이벤트
/// </summary>
public static event System.Action<BossEnemy> OnBossSpawned;
/// <summary>
/// 현재 활성화된 보스 (Scene에 하나만 존재한다고 가정)
/// </summary>
public static BossEnemy ActiveBoss { get; private set; }
// Properties
public int CurrentPhaseIndex => currentPhaseIndex;
public BossPhaseData CurrentPhase => phases.Count > currentPhaseIndex ? phases[currentPhaseIndex] : null;
public int TotalPhases => phases.Count;
public bool IsTransitioning => isTransitioning;
public float PhaseElapsedTime => phaseElapsedTime;
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
// BehaviorGraphAgent 컴포넌트 확인/추가
behaviorAgent = GetComponent<BehaviorGraphAgent>();
if (behaviorAgent == null)
{
behaviorAgent = gameObject.AddComponent<BehaviorGraphAgent>();
}
// 초기 AI 설정
if (IsServer && initialBehaviorGraph != null)
{
behaviorAgent.Graph = initialBehaviorGraph;
}
// 정적 이벤트 발생 (UI 자동 연결용)
ActiveBoss = this;
OnBossSpawned?.Invoke(this);
if (debugMode)
{
Debug.Log($"[Boss] Boss spawned: {name}");
}
}
protected override void InitializeStats()
{
base.InitializeStats();
phaseStartTime = Time.time;
phaseElapsedTime = 0f;
currentPhaseIndex = 0;
isTransitioning = false;
isInvincible = false;
customConditions.Clear();
}
private void Update()
{
if (!IsServer || IsDead || isTransitioning)
return;
phaseElapsedTime = Time.time - phaseStartTime;
// 다음 페이즈 전환 조건 확인
CheckPhaseTransition();
}
/// <summary>
/// 페이즈 전환 조건 확인
/// </summary>
private void CheckPhaseTransition()
{
int nextPhaseIndex = currentPhaseIndex + 1;
if (nextPhaseIndex >= phases.Count)
return;
BossPhaseData nextPhase = phases[nextPhaseIndex];
if (nextPhase == null)
return;
if (nextPhase.CheckTransitionCondition(this, phaseElapsedTime))
{
StartPhaseTransition(nextPhaseIndex);
}
}
/// <summary>
/// 페이즈 전환 시작
/// </summary>
private void StartPhaseTransition(int newPhaseIndex)
{
if (newPhaseIndex >= phases.Count || isTransitioning)
return;
isTransitioning = true;
isInvincible = true;
StartCoroutine(PhaseTransitionCoroutine(newPhaseIndex));
}
private System.Collections.IEnumerator PhaseTransitionCoroutine(int newPhaseIndex)
{
BossPhaseData newPhase = phases[newPhaseIndex];
// 전환 이벤트
OnPhaseTransitionStart?.Invoke(phaseTransitionDuration);
// 전환 연출
yield return PlayPhaseTransitionEffect(newPhase);
// AI 그래프 교체
if (newPhase.BehaviorGraph != null && behaviorAgent != null)
{
behaviorAgent.End();
behaviorAgent.Graph = newPhase.BehaviorGraph;
}
// 페이즈 전환 완료
currentPhaseIndex = newPhaseIndex;
phaseStartTime = Time.time;
phaseElapsedTime = 0f;
// 무적 해제
yield return new WaitForSeconds(phaseTransitionInvincibilityTime);
isInvincible = false;
isTransitioning = false;
OnPhaseTransitionEnd?.Invoke();
OnPhaseChanged?.Invoke(currentPhaseIndex);
if (debugMode)
{
Debug.Log($"[Boss] Phase transition: {currentPhaseIndex} ({newPhase.PhaseName})");
}
}
/// <summary>
/// 페이즈 전환 연출 재생
/// </summary>
private System.Collections.IEnumerator PlayPhaseTransitionEffect(BossPhaseData newPhase)
{
// 애니메이션 재생
if (animator != null && newPhase.PhaseStartAnimation != null)
{
animator.Play(newPhase.PhaseStartAnimation.name);
}
// 이펙트 생성
if (newPhase.PhaseTransitionEffect != null)
{
var effect = Instantiate(newPhase.PhaseTransitionEffect, transform.position, transform.rotation);
Destroy(effect, phaseTransitionDuration);
}
// 전환 시간 대기
yield return new WaitForSeconds(phaseTransitionDuration);
}
/// <summary>
/// 대미지 적용 (무적 상태 고려)
/// </summary>
public override float TakeDamage(float damage, object source = null)
{
if (isInvincible)
return 0f;
return base.TakeDamage(damage, source);
}
/// <summary>
/// 커스텀 조건 설정
/// </summary>
public void SetCustomCondition(string conditionId, bool value)
{
customConditions[conditionId] = value;
}
/// <summary>
/// 커스텀 조건 확인
/// </summary>
public bool CheckCustomCondition(string conditionId)
{
return customConditions.TryGetValue(conditionId, out bool value) && value;
}
/// <summary>
/// 수동으로 페이즈 전환
/// </summary>
public void ForcePhaseTransition(int phaseIndex)
{
if (!IsServer)
return;
if (phaseIndex >= 0 && phaseIndex < phases.Count && phaseIndex != currentPhaseIndex)
{
StartPhaseTransition(phaseIndex);
}
}
/// <summary>
/// 현재 페이즈 재시작
/// </summary>
public void RestartCurrentPhase()
{
if (!IsServer)
return;
phaseStartTime = Time.time;
phaseElapsedTime = 0f;
if (behaviorAgent != null)
{
behaviorAgent.Restart();
}
}
protected override void HandleDeath()
{
// 마지막 페이즈에서만 사망 처리
if (currentPhaseIndex < phases.Count - 1 && !isTransitioning)
{
// 아직 페이즈가 남아있으면 강제로 다음 페이즈로
StartPhaseTransition(currentPhaseIndex + 1);
return;
}
// AI 완전 중단 (순서 중요: enabled=false를 먼저 호출하여 Update() 차단)
if (behaviorAgent != null)
{
behaviorAgent.enabled = false; // 가장 먼저: Update() 호출 방지
behaviorAgent.End();
behaviorAgent.Graph = null;
}
behaviorAgent = null;
base.HandleDeath();
}
#region Debug
private void OnDrawGizmosSelected()
{
if (!debugMode)
return;
// 현재 페이즈 정보 표시
#if UNITY_EDITOR
if (phases != null && currentPhaseIndex < phases.Count)
{
var phase = phases[currentPhaseIndex];
if (phase != null)
{
UnityEditor.Handles.Label(
transform.position + Vector3.up * 3f,
$"Phase {currentPhaseIndex + 1}/{phases.Count}\n{phase.PhaseName}"
);
}
}
#endif
}
#endregion
}
}