- 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>
316 lines
9.8 KiB
C#
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
|
|
}
|
|
}
|