[Enemy] 보스 적 시스템 구현
Ultraworked with [Sisyphus](https://github.com/code-yeonggu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
291
Assets/Scripts/Enemy/BossEnemy.cs
Normal file
291
Assets/Scripts/Enemy/BossEnemy.cs
Normal file
@@ -0,0 +1,291 @@
|
||||
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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
base.HandleDeath();
|
||||
|
||||
// AI 정지
|
||||
if (behaviorAgent != null)
|
||||
{
|
||||
behaviorAgent.End();
|
||||
}
|
||||
}
|
||||
|
||||
#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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user