feat: 드로그 보스 AI 및 런타임 상태 구조 재구성
- 드로그 전투 컨텍스트를 BossBehaviorRuntimeState 중심 구조로 정리하고 BossEnemy, 패턴 액션, 조건 노드가 마지막 실행 결과와 phase 상태를 직접 사용하도록 갱신 - BT_Drog와 재빌드 에디터 스크립트를 확장해 phase 전환, 집행 결과 분기, 거리/쿨타임 기반 패턴 선택을 드로그 전용 자산과 노드 파라미터로 재구성 - 드로그 패턴/스킬/이펙트/애니메이션 플레이스홀더 자산을 재생성하고 보스 프리팹이 새 런타임 상태 및 등록 클립 구성을 참조하도록 정리
This commit is contained in:
418
Assets/_Game/Scripts/Enemy/BossBehaviorRuntimeState.cs
Normal file
418
Assets/_Game/Scripts/Enemy/BossBehaviorRuntimeState.cs
Normal file
@@ -0,0 +1,418 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Colosseum.AI;
|
||||
using Colosseum.Abnormalities;
|
||||
using Colosseum.Skills;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Colosseum.Enemy
|
||||
{
|
||||
/// <summary>
|
||||
/// 마지막 패턴 실행 결과입니다.
|
||||
/// </summary>
|
||||
public enum BossPatternExecutionResult
|
||||
{
|
||||
None,
|
||||
Running,
|
||||
Succeeded,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 보스 BT가 읽고 쓰는 런타임 전투 상태를 보관합니다.
|
||||
/// 타겟, 페이즈 진행 상태, 패턴 쿨다운 같은 전투 런타임 결과를 외부 시스템과 공유합니다.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(BossEnemy))]
|
||||
[RequireComponent(typeof(SkillController))]
|
||||
public class BossBehaviorRuntimeState : NetworkBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] protected BossEnemy bossEnemy;
|
||||
[SerializeField] protected EnemyBase enemyBase;
|
||||
[SerializeField] protected SkillController skillController;
|
||||
[SerializeField] protected AbnormalityManager abnormalityManager;
|
||||
[SerializeField] protected UnityEngine.AI.NavMeshAgent navMeshAgent;
|
||||
[SerializeField] protected BehaviorGraphAgent behaviorGraphAgent;
|
||||
|
||||
[Header("Pattern Flow")]
|
||||
[Tooltip("대형 패턴(시그니처/기동/조합) 직후 기본 패턴 최소 순환 횟수")]
|
||||
[Min(0)] [SerializeField] protected int basicLoopMinCountAfterBigPattern = 2;
|
||||
|
||||
[Header("시그니처 효과 설정")]
|
||||
[Tooltip("시그니처 패턴 차단에 필요한 누적 피해 비율")]
|
||||
[Range(0f, 1f)] [SerializeField] protected float signatureRequiredDamageRatio = 0.1f;
|
||||
|
||||
[Tooltip("시그니처 준비 상태를 나타내는 이상상태")]
|
||||
[SerializeField] protected AbnormalityData signatureTelegraphAbnormality;
|
||||
|
||||
[Tooltip("시그니처 차단 성공 시 보스가 멈추는 시간")]
|
||||
[Min(0f)] [SerializeField] protected float signatureSuccessStaggerDuration = 2f;
|
||||
|
||||
[Tooltip("시그니처 실패 시 모든 플레이어에게 적용할 디버프")]
|
||||
[SerializeField] protected AbnormalityData signatureFailureAbnormality;
|
||||
|
||||
[Tooltip("시그니처 실패 시 모든 플레이어에게 주는 기본 피해")]
|
||||
[Min(0f)] [SerializeField] protected float signatureFailureDamage = 40f;
|
||||
|
||||
[Tooltip("시그니처 실패 시 넉백이 적용되는 반경")]
|
||||
[Min(0f)] [SerializeField] protected float signatureFailureKnockbackRadius = 8f;
|
||||
|
||||
[Tooltip("시그니처 실패 시 다운이 적용되는 반경")]
|
||||
[Min(0f)] [SerializeField] protected float signatureFailureDownRadius = 3f;
|
||||
|
||||
[Tooltip("시그니처 실패 시 넉백 속도")]
|
||||
[Min(0f)] [SerializeField] protected float signatureFailureKnockbackSpeed = 12f;
|
||||
|
||||
[Tooltip("시그니처 실패 시 넉백 지속 시간")]
|
||||
[Min(0f)] [SerializeField] protected float signatureFailureKnockbackDuration = 0.35f;
|
||||
|
||||
[Tooltip("시그니처 실패 시 다운 지속 시간")]
|
||||
[Min(0f)] [SerializeField] protected float signatureFailureDownDuration = 2f;
|
||||
|
||||
[Header("Phase State")]
|
||||
[Tooltip("BT가 관리하는 최대 페이즈 수")]
|
||||
[Min(1)] [SerializeField] protected int maxPatternPhase = 3;
|
||||
|
||||
[Tooltip("디버그 로그 출력 여부")]
|
||||
[SerializeField] protected bool debugMode = false;
|
||||
|
||||
protected readonly Dictionary<BossPatternData, float> patternCooldownTracker = new Dictionary<BossPatternData, float>();
|
||||
protected readonly Dictionary<string, bool> customPhaseConditions = new Dictionary<string, bool>();
|
||||
|
||||
protected GameObject currentTarget;
|
||||
protected int meleePatternCounter;
|
||||
protected int basicLoopCountSinceLastBigPattern;
|
||||
protected int currentPatternPhase = 1;
|
||||
protected float currentPhaseStartTime;
|
||||
protected BossPatternExecutionResult lastPatternExecutionResult;
|
||||
protected BossPatternData lastExecutedPattern;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 전투 대상
|
||||
/// </summary>
|
||||
public GameObject CurrentTarget => currentTarget;
|
||||
|
||||
/// <summary>
|
||||
/// BT가 관리하는 현재 페이즈
|
||||
/// </summary>
|
||||
public int CurrentPatternPhase => Mathf.Clamp(currentPatternPhase, 1, Mathf.Max(1, maxPatternPhase));
|
||||
|
||||
/// <summary>
|
||||
/// BT가 관리하는 최대 페이즈 수
|
||||
/// </summary>
|
||||
public int MaxPatternPhase => Mathf.Max(1, maxPatternPhase);
|
||||
|
||||
/// <summary>
|
||||
/// 현재 페이즈의 경과 시간
|
||||
/// </summary>
|
||||
public float PhaseElapsedTime => Time.time - currentPhaseStartTime;
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 대형 패턴 이후 누적된 기본 루프 횟수
|
||||
/// </summary>
|
||||
public int BasicLoopCountSinceLastBigPattern => basicLoopCountSinceLastBigPattern;
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 패턴 실행 결과
|
||||
/// </summary>
|
||||
public BossPatternExecutionResult LastPatternExecutionResult => lastPatternExecutionResult;
|
||||
|
||||
/// <summary>
|
||||
/// 마지막으로 실행한 패턴
|
||||
/// </summary>
|
||||
public BossPatternData LastExecutedPattern => lastExecutedPattern;
|
||||
|
||||
/// <summary>
|
||||
/// EnemyBase 접근자
|
||||
/// </summary>
|
||||
public EnemyBase EnemyBase => enemyBase;
|
||||
|
||||
/// <summary>
|
||||
/// 디버그 로그 출력 여부
|
||||
/// </summary>
|
||||
public bool DebugModeEnabled => debugMode;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 실패 시 모든 플레이어에게 주는 기본 피해
|
||||
/// </summary>
|
||||
public float SignatureFailureDamage => signatureFailureDamage;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 실패 시 모든 플레이어에게 적용할 디버프
|
||||
/// </summary>
|
||||
public AbnormalityData SignatureFailureAbnormality => signatureFailureAbnormality;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 실패 시 넉백이 적용되는 반경
|
||||
/// </summary>
|
||||
public float SignatureFailureKnockbackRadius => signatureFailureKnockbackRadius;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 실패 시 다운이 적용되는 반경
|
||||
/// </summary>
|
||||
public float SignatureFailureDownRadius => signatureFailureDownRadius;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 실패 시 넉백 속도
|
||||
/// </summary>
|
||||
public float SignatureFailureKnockbackSpeed => signatureFailureKnockbackSpeed;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 실패 시 넉백 지속 시간
|
||||
/// </summary>
|
||||
public float SignatureFailureKnockbackDuration => signatureFailureKnockbackDuration;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 실패 시 다운 지속 시간
|
||||
/// </summary>
|
||||
public float SignatureFailureDownDuration => signatureFailureDownDuration;
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 충전 차단 시 설정된 경직 시간 (BossPatternActionBase가 설정)
|
||||
/// </summary>
|
||||
public float LastChargeStaggerDuration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 패턴 실행에서 충전이 차단되었는지 여부.
|
||||
/// BT 노드(IsChargeBrokenCondition)에서 판독합니다.
|
||||
/// </summary>
|
||||
public bool WasChargeBroken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기절 등으로 인해 보스 전투 로직을 진행할 수 없는 상태인지 여부
|
||||
/// </summary>
|
||||
public bool IsBehaviorSuppressed => abnormalityManager != null && abnormalityManager.IsStunned;
|
||||
|
||||
/// <summary>
|
||||
protected virtual void Awake()
|
||||
{
|
||||
ResolveReferences();
|
||||
ResetPhaseState();
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
ResolveReferences();
|
||||
ResetPhaseState();
|
||||
|
||||
if (!IsServer)
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
protected virtual void Update()
|
||||
{
|
||||
if (!IsServer)
|
||||
return;
|
||||
|
||||
ResolveReferences();
|
||||
if (bossEnemy == null || enemyBase == null || skillController == null)
|
||||
return;
|
||||
|
||||
if (bossEnemy.IsDead)
|
||||
return;
|
||||
|
||||
if (IsBehaviorSuppressed)
|
||||
StopMovement();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BT가 선택한 현재 전투 대상을 동기화합니다.
|
||||
/// </summary>
|
||||
public void SetCurrentTarget(GameObject target)
|
||||
{
|
||||
currentTarget = target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BT가 현재 페이즈 값을 갱신합니다.
|
||||
/// 필요하면 경과 시간 기준도 함께 초기화합니다.
|
||||
/// </summary>
|
||||
public void SetCurrentPatternPhase(int phase, bool resetTimer = true)
|
||||
{
|
||||
currentPatternPhase = Mathf.Clamp(phase, 1, MaxPatternPhase);
|
||||
|
||||
if (resetTimer)
|
||||
currentPhaseStartTime = Time.time;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 페이즈 타이머를 다시 시작합니다.
|
||||
/// </summary>
|
||||
public void RestartCurrentPhaseTimer()
|
||||
{
|
||||
currentPhaseStartTime = Time.time;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 패턴 실행 시작을 기록합니다.
|
||||
/// </summary>
|
||||
public void BeginPatternExecution(BossPatternData pattern)
|
||||
{
|
||||
lastExecutedPattern = pattern;
|
||||
lastPatternExecutionResult = BossPatternExecutionResult.Running;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 패턴 실행 결과를 기록합니다.
|
||||
/// </summary>
|
||||
public void CompletePatternExecution(BossPatternData pattern, BossPatternExecutionResult result)
|
||||
{
|
||||
lastExecutedPattern = pattern;
|
||||
lastPatternExecutionResult = result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 페이즈 커스텀 조건을 기록합니다.
|
||||
/// </summary>
|
||||
public void SetPhaseCustomCondition(string conditionId, bool value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(conditionId))
|
||||
return;
|
||||
|
||||
customPhaseConditions[conditionId] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 페이즈 커스텀 조건 값을 읽습니다.
|
||||
/// </summary>
|
||||
public bool CheckPhaseCustomCondition(string conditionId)
|
||||
{
|
||||
return !string.IsNullOrEmpty(conditionId)
|
||||
&& customPhaseConditions.TryGetValue(conditionId, out bool value)
|
||||
&& value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 근접 패턴 사용 카운터를 갱신합니다.
|
||||
/// </summary>
|
||||
public void RegisterPatternUse(BossPatternData pattern)
|
||||
{
|
||||
if (pattern == null)
|
||||
return;
|
||||
|
||||
if (pattern.IsMelee)
|
||||
{
|
||||
meleePatternCounter++;
|
||||
basicLoopCountSinceLastBigPattern++;
|
||||
}
|
||||
|
||||
if (pattern.Category == PatternCategory.Punish || pattern.IsBigPattern)
|
||||
{
|
||||
basicLoopCountSinceLastBigPattern = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 로그를 출력합니다.
|
||||
/// </summary>
|
||||
public void LogDebug(string source, string message)
|
||||
{
|
||||
if (debugMode)
|
||||
Debug.Log($"[{source}] {message}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정 패턴이 grace period를 통과했는지 반환합니다.
|
||||
/// Punish/Melee/Utility는 항상 허용됩니다.
|
||||
/// </summary>
|
||||
public bool IsPatternGracePeriodAllowed(BossPatternData pattern)
|
||||
{
|
||||
if (pattern == null)
|
||||
return false;
|
||||
|
||||
if (pattern.Category == PatternCategory.Punish)
|
||||
return true;
|
||||
|
||||
if (pattern.IsMelee || pattern.TargetMode == TargetResolveMode.Utility)
|
||||
return true;
|
||||
|
||||
return basicLoopCountSinceLastBigPattern >= basicLoopMinCountAfterBigPattern;
|
||||
}
|
||||
|
||||
public bool IsPatternReady(BossPatternData pattern)
|
||||
{
|
||||
if (pattern == null || pattern.Steps == null || pattern.Steps.Count == 0)
|
||||
return false;
|
||||
|
||||
if (!patternCooldownTracker.TryGetValue(pattern, out float readyTime))
|
||||
return true;
|
||||
|
||||
return Time.time >= readyTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 패턴 쿨다운을 설정합니다. BT 노드(BossPatternActionBase)와 코드 폴백 모두에서 호출합니다.
|
||||
/// </summary>
|
||||
public void SetPatternCooldown(BossPatternData pattern)
|
||||
{
|
||||
if (pattern != null)
|
||||
patternCooldownTracker[pattern] = Time.time + pattern.Cooldown;
|
||||
}
|
||||
|
||||
protected void StopMovement()
|
||||
{
|
||||
if (navMeshAgent == null || !navMeshAgent.enabled)
|
||||
return;
|
||||
|
||||
navMeshAgent.isStopped = true;
|
||||
navMeshAgent.ResetPath();
|
||||
}
|
||||
|
||||
protected virtual void ResolveReferences()
|
||||
{
|
||||
if (bossEnemy == null)
|
||||
bossEnemy = GetComponent<BossEnemy>();
|
||||
|
||||
if (enemyBase == null)
|
||||
enemyBase = GetComponent<EnemyBase>();
|
||||
|
||||
if (skillController == null)
|
||||
skillController = GetComponent<SkillController>();
|
||||
|
||||
if (abnormalityManager == null)
|
||||
abnormalityManager = GetComponent<AbnormalityManager>();
|
||||
|
||||
if (navMeshAgent == null)
|
||||
navMeshAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
|
||||
|
||||
if (behaviorGraphAgent == null)
|
||||
behaviorGraphAgent = GetComponent<BehaviorGraphAgent>();
|
||||
}
|
||||
|
||||
public virtual void ResetPhaseState()
|
||||
{
|
||||
currentPatternPhase = 1;
|
||||
currentPhaseStartTime = Time.time;
|
||||
lastPatternExecutionResult = BossPatternExecutionResult.None;
|
||||
lastExecutedPattern = null;
|
||||
customPhaseConditions.Clear();
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
base.OnNetworkDespawn();
|
||||
}
|
||||
|
||||
private static bool HasAnimatorParameter(Animator animator, string parameterName, AnimatorControllerParameterType parameterType)
|
||||
{
|
||||
if (animator == null || string.IsNullOrEmpty(parameterName))
|
||||
return false;
|
||||
|
||||
AnimatorControllerParameter[] parameters = animator.parameters;
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
AnimatorControllerParameter parameter = parameters[i];
|
||||
if (parameter.type == parameterType && parameter.name == parameterName)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user