refactor: 드로그 BT 의사결정 투명화 — 모든 조건을 BT 노드로 표시
- BossCombatPatternRole enum 완전 제거, BossPatternData에 직접 필드 추가 - 14개 패턴별 Check*/Use*Action → CheckPatternReadyCondition + UsePatternByRoleAction으로 통합 - BT 계단식 Branch 체인 구조 도입 (BranchingConditionComposite + FloatingPort) - 패턴별 고유 전제 조건을 BT Condition으로 분리 - Punish: IsDownedTargetInRangeCondition (다운 대상 반경) - Mobility: IsTargetBeyondDistanceCondition (원거리 대상) - Utility: IsTargetBeyondDistanceCondition (원거리 대상) - Primary: IsTargetInAttackRangeCondition (사거리 이내) - Phase 진입 조건을 BT에서 확인 가능하도록 IsMinPhaseSatisfiedCondition 추가 - IsPatternReady()에서 minPhase 체크 분리 → 전용 Condition으로 노출 - Secondary 패턴 개념 제거 (secondaryPattern, 보조 차례, 교대 카운터 로직 전부 삭제) - CanResolvePatternTargetCondition 삭제 (7개 중 5개가 노이즈) - RebuildDrogBehaviorAuthoringGraph로 BT 에셋 자동 재구성 메뉴 제공
This commit is contained in:
@@ -36,10 +36,6 @@ namespace Colosseum.Enemy
|
||||
[FormerlySerializedAs("mainPattern")]
|
||||
[SerializeField] protected BossPatternData primaryPattern;
|
||||
|
||||
[Tooltip("보조 근접 압박 패턴")]
|
||||
[FormerlySerializedAs("slamPattern")]
|
||||
[SerializeField] protected BossPatternData secondaryPattern;
|
||||
|
||||
[Tooltip("기동 또는 거리 징벌 패턴")]
|
||||
[FormerlySerializedAs("leapPattern")]
|
||||
[SerializeField] protected BossPatternData mobilityPattern;
|
||||
@@ -47,6 +43,9 @@ namespace Colosseum.Enemy
|
||||
[Tooltip("비주 대상 원거리 견제 패턴")]
|
||||
[SerializeField] protected BossPatternData utilityPattern;
|
||||
|
||||
[Tooltip("Phase 3 조합 패턴")]
|
||||
[SerializeField] protected BossPatternData comboPattern;
|
||||
|
||||
[Tooltip("특정 상황에서 우선 발동하는 징벌 패턴")]
|
||||
[FormerlySerializedAs("downPunishPattern")]
|
||||
[SerializeField] protected BossPatternData punishPattern;
|
||||
@@ -77,18 +76,9 @@ namespace Colosseum.Enemy
|
||||
[Tooltip("원거리 견제 패턴을 고려하기 시작하는 최소 거리")]
|
||||
[Min(0f)] [SerializeField] protected float utilityTriggerDistance = 5f;
|
||||
|
||||
[Header("Pattern Cadence")]
|
||||
[Tooltip("1페이즈에서 몇 번의 근접 패턴마다 보조 패턴을 섞을지")]
|
||||
[FormerlySerializedAs("phase1SlamInterval")]
|
||||
[Min(1)] [SerializeField] protected int phase1SecondaryInterval = 3;
|
||||
|
||||
[Tooltip("2페이즈에서 몇 번의 근접 패턴마다 보조 패턴을 섞을지")]
|
||||
[FormerlySerializedAs("phase2SlamInterval")]
|
||||
[Min(1)] [SerializeField] protected int phase2SecondaryInterval = 2;
|
||||
|
||||
[Tooltip("3페이즈에서 몇 번의 근접 패턴마다 보조 패턴을 섞을지")]
|
||||
[FormerlySerializedAs("phase3SlamInterval")]
|
||||
[Min(1)] [SerializeField] protected int phase3SecondaryInterval = 2;
|
||||
[Header("Pattern Flow")]
|
||||
[Tooltip("대형 패턴(시그니처/기동/조합) 직후 기본 패턴 최소 순환 횟수")]
|
||||
[Min(0)] [SerializeField] protected int basicLoopMinCountAfterBigPattern = 2;
|
||||
|
||||
[Header("Signature Pattern")]
|
||||
[Tooltip("시그니처 패턴 차단에 필요한 누적 피해 비율")]
|
||||
@@ -122,8 +112,8 @@ namespace Colosseum.Enemy
|
||||
[Min(0f)] [SerializeField] protected float signatureFailureDownDuration = 2f;
|
||||
|
||||
[Header("Behavior")]
|
||||
[Tooltip("전용 컨텍스트 사용 시 기존 BehaviorGraph를 비활성화할지 여부")]
|
||||
[SerializeField] protected bool disableBehaviorGraph = true;
|
||||
[Tooltip("true면 컨텍스트 코드가 AI를 직접 구동합니다. false면 BehaviorGraph가 모든 의사결정을 담당합니다.")]
|
||||
[SerializeField] protected bool disableBehaviorGraph = false;
|
||||
|
||||
[Tooltip("디버그 로그 출력 여부")]
|
||||
[SerializeField] protected bool debugMode = false;
|
||||
@@ -140,6 +130,7 @@ namespace Colosseum.Enemy
|
||||
protected float signatureRequiredDamage;
|
||||
protected float signatureElapsedTime;
|
||||
protected float signatureTotalDuration;
|
||||
protected int basicLoopCountSinceLastBigPattern;
|
||||
|
||||
/// <summary>
|
||||
/// 전용 컨텍스트 사용 시 BehaviorGraph를 비활성화할지 여부
|
||||
@@ -270,66 +261,50 @@ namespace Colosseum.Enemy
|
||||
if (skillController.IsPlayingAnimation)
|
||||
return;
|
||||
|
||||
// 1. 다운 추가타 (최우선 인터럽트, grace period 면제)
|
||||
if (TryStartPunishPattern())
|
||||
return;
|
||||
|
||||
// 2. 집행 개시 (Phase 3 시그니처)
|
||||
if (TryStartSignaturePatternInLoop())
|
||||
return;
|
||||
|
||||
// 3. 조합 패턴 (Phase 3, 드물게)
|
||||
if (TryStartComboPattern())
|
||||
return;
|
||||
|
||||
// 4. 기동 패턴 (거리 기반 조건부)
|
||||
if (TryStartMobilityPattern())
|
||||
return;
|
||||
|
||||
// 5. 원거리 견제 (보조)
|
||||
if (TryStartUtilityPattern())
|
||||
return;
|
||||
|
||||
// 6. 기본 루프
|
||||
TryStartPrimaryLoopPattern();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 역할의 패턴 데이터를 반환합니다.
|
||||
/// </summary>
|
||||
public BossPatternData GetPattern(BossCombatPatternRole role)
|
||||
{
|
||||
return role switch
|
||||
{
|
||||
BossCombatPatternRole.Primary => primaryPattern,
|
||||
BossCombatPatternRole.Secondary => secondaryPattern,
|
||||
BossCombatPatternRole.Mobility => mobilityPattern,
|
||||
BossCombatPatternRole.Utility => utilityPattern,
|
||||
BossCombatPatternRole.Punish => punishPattern,
|
||||
BossCombatPatternRole.Signature => signaturePattern,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다음 근접 패턴 차례가 보조 패턴인지 여부
|
||||
/// </summary>
|
||||
public bool IsNextSecondaryPattern()
|
||||
{
|
||||
int secondaryInterval = GetSecondaryIntervalForPhase(CurrentPatternPhase);
|
||||
if (secondaryInterval <= 1)
|
||||
return true;
|
||||
|
||||
return (meleePatternCounter + 1) % secondaryInterval == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 페이즈 기준의 보조 근접 패턴 주기를 반환합니다.
|
||||
/// </summary>
|
||||
public int GetSecondaryIntervalForPhase(int phase)
|
||||
{
|
||||
return phase switch
|
||||
{
|
||||
1 => Mathf.Max(1, phase1SecondaryInterval),
|
||||
2 => Mathf.Max(1, phase2SecondaryInterval),
|
||||
_ => Mathf.Max(1, phase3SecondaryInterval),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 근접 패턴 사용 카운터를 갱신합니다.
|
||||
/// </summary>
|
||||
public void RegisterPatternUse(BossCombatPatternRole role)
|
||||
public void RegisterPatternUse(BossPatternData pattern)
|
||||
{
|
||||
if (!role.IsMeleeRole())
|
||||
if (pattern == null)
|
||||
return;
|
||||
|
||||
meleePatternCounter++;
|
||||
if (pattern.IsMelee)
|
||||
{
|
||||
meleePatternCounter++;
|
||||
basicLoopCountSinceLastBigPattern++;
|
||||
}
|
||||
|
||||
if (pattern.Category == PatternCategory.Punish || pattern.IsBigPattern)
|
||||
{
|
||||
basicLoopCountSinceLastBigPattern = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -500,9 +475,53 @@ namespace Colosseum.Enemy
|
||||
if (bossEnemy.IsDead || bossEnemy.IsTransitioning || skillController.IsPlayingAnimation)
|
||||
return false;
|
||||
|
||||
if (!IsPatternGracePeriodAllowed(signaturePattern))
|
||||
return false;
|
||||
|
||||
return IsPatternReady(signaturePattern);
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 조합 패턴 사용 가능 여부를 반환합니다.
|
||||
/// </summary>
|
||||
public bool IsComboPatternReady()
|
||||
{
|
||||
if (!IsServer || bossEnemy == null || skillController == null)
|
||||
return false;
|
||||
|
||||
if (IsBehaviorSuppressed)
|
||||
return false;
|
||||
|
||||
if (activePatternCoroutine != null || isSignaturePatternActive)
|
||||
return false;
|
||||
|
||||
if (bossEnemy.IsDead || bossEnemy.IsTransitioning || skillController.IsPlayingAnimation)
|
||||
return false;
|
||||
|
||||
if (!IsPatternGracePeriodAllowed(comboPattern))
|
||||
return false;
|
||||
|
||||
return IsPatternReady(comboPattern);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 패턴을 시작합니다.
|
||||
/// </summary>
|
||||
@@ -566,7 +585,7 @@ namespace Colosseum.Enemy
|
||||
|
||||
protected virtual bool TryStartMobilityPattern()
|
||||
{
|
||||
BossPatternData pattern = GetPattern(BossCombatPatternRole.Mobility);
|
||||
BossPatternData pattern = mobilityPattern;
|
||||
if (!IsPatternReady(pattern))
|
||||
return false;
|
||||
|
||||
@@ -581,7 +600,7 @@ namespace Colosseum.Enemy
|
||||
|
||||
protected virtual bool TryStartUtilityPattern()
|
||||
{
|
||||
BossPatternData pattern = GetPattern(BossCombatPatternRole.Utility);
|
||||
BossPatternData pattern = utilityPattern;
|
||||
if (!IsPatternReady(pattern))
|
||||
return false;
|
||||
|
||||
@@ -594,33 +613,109 @@ namespace Colosseum.Enemy
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다운 대상이 존재하면 징벌 패턴을 발동합니다.
|
||||
/// </summary>
|
||||
protected virtual bool TryStartPunishPattern()
|
||||
{
|
||||
BossPatternData pattern = punishPattern;
|
||||
if (!IsPatternReady(pattern))
|
||||
return false;
|
||||
|
||||
HitReactionController[] hitReactionControllers = FindObjectsByType<HitReactionController>(FindObjectsSortMode.None);
|
||||
GameObject nearestDownedTarget = null;
|
||||
float nearestDistance = float.MaxValue;
|
||||
|
||||
for (int i = 0; i < hitReactionControllers.Length; i++)
|
||||
{
|
||||
HitReactionController controller = hitReactionControllers[i];
|
||||
if (controller == null || !controller.IsDowned)
|
||||
continue;
|
||||
|
||||
GameObject candidate = controller.gameObject;
|
||||
if (candidate == null || !candidate.activeInHierarchy)
|
||||
continue;
|
||||
|
||||
if (Team.IsSameTeam(gameObject, candidate))
|
||||
continue;
|
||||
|
||||
IDamageable damageable = candidate.GetComponent<IDamageable>();
|
||||
if (damageable != null && damageable.IsDead)
|
||||
continue;
|
||||
|
||||
float distance = Vector3.Distance(transform.position, candidate.transform.position);
|
||||
if (distance > punishSearchRadius || distance >= nearestDistance)
|
||||
continue;
|
||||
|
||||
nearestDistance = distance;
|
||||
nearestDownedTarget = candidate;
|
||||
}
|
||||
|
||||
if (nearestDownedTarget == null)
|
||||
return false;
|
||||
|
||||
currentTarget = nearestDownedTarget;
|
||||
StartPattern(pattern, nearestDownedTarget);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 패턴을 context 루프에서 발동합니다.
|
||||
/// grace period와 Phase 제한을 적용합니다.
|
||||
/// </summary>
|
||||
protected virtual bool TryStartSignaturePatternInLoop()
|
||||
{
|
||||
if (!IsSignaturePatternReady())
|
||||
return false;
|
||||
|
||||
if (!IsPatternGracePeriodAllowed(signaturePattern))
|
||||
return false;
|
||||
|
||||
GameObject target = ResolvePrimaryTarget();
|
||||
return TryStartSignaturePattern(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase 3 조합 패턴을 발동합니다.
|
||||
/// </summary>
|
||||
protected virtual bool TryStartComboPattern()
|
||||
{
|
||||
if (!IsComboPatternReady())
|
||||
return false;
|
||||
|
||||
currentTarget = ResolvePrimaryTarget();
|
||||
StartPattern(comboPattern, currentTarget);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual BossPatternData SelectPrimaryLoopPattern()
|
||||
{
|
||||
BossPatternData primary = GetPattern(BossCombatPatternRole.Primary);
|
||||
BossPatternData secondary = GetPattern(BossCombatPatternRole.Secondary);
|
||||
if (!IsPatternReady(primaryPattern))
|
||||
return null;
|
||||
|
||||
bool canUsePrimary = IsPatternReady(primary);
|
||||
bool canUseSecondary = IsPatternReady(secondary);
|
||||
meleePatternCounter++;
|
||||
return primaryPattern;
|
||||
}
|
||||
|
||||
if (canUseSecondary && IsNextSecondaryPattern())
|
||||
{
|
||||
meleePatternCounter++;
|
||||
return secondary;
|
||||
}
|
||||
/// <summary>
|
||||
/// 기본 패턴을 선택하고 카운터를 갱신합니다.
|
||||
/// </summary>
|
||||
public BossPatternData SelectAndRegisterBasicLoopPattern()
|
||||
{
|
||||
if (!IsPatternReady(primaryPattern))
|
||||
return null;
|
||||
|
||||
if (canUsePrimary)
|
||||
{
|
||||
meleePatternCounter++;
|
||||
return primary;
|
||||
}
|
||||
RegisterPatternUse(primaryPattern);
|
||||
return primaryPattern;
|
||||
}
|
||||
|
||||
if (canUseSecondary)
|
||||
{
|
||||
meleePatternCounter++;
|
||||
return secondary;
|
||||
}
|
||||
|
||||
return null;
|
||||
/// <summary>
|
||||
/// 기본 패턴이 사용 가능한지 확인합니다.
|
||||
/// 상태 변경 없이 순수 검사만 수행합니다.
|
||||
/// </summary>
|
||||
public bool IsBasicLoopReady()
|
||||
{
|
||||
return IsPatternReady(primaryPattern);
|
||||
}
|
||||
|
||||
protected virtual void StartPattern(BossPatternData pattern, GameObject target)
|
||||
@@ -655,9 +750,18 @@ namespace Colosseum.Enemy
|
||||
break;
|
||||
}
|
||||
|
||||
if (step.Skill.JumpToTarget && target != null)
|
||||
if (step.Skill.JumpToTarget)
|
||||
{
|
||||
enemyBase?.SetJumpTarget(target.transform.position);
|
||||
GameObject jumpTarget = FindMobilityTarget();
|
||||
if (jumpTarget == null)
|
||||
{
|
||||
LogDebug(GetType().Name, $"점프 대상 없음, 패턴 조기 종료: {pattern.PatternName}");
|
||||
break;
|
||||
}
|
||||
|
||||
target = jumpTarget;
|
||||
currentTarget = jumpTarget;
|
||||
enemyBase?.SetJumpTarget(jumpTarget.transform.position);
|
||||
}
|
||||
|
||||
if (!skillController.ExecuteSkill(step.Skill))
|
||||
@@ -686,9 +790,6 @@ namespace Colosseum.Enemy
|
||||
if (pattern == null || pattern.Steps == null || pattern.Steps.Count == 0)
|
||||
return false;
|
||||
|
||||
if (CurrentPatternPhase < pattern.MinPhase)
|
||||
return false;
|
||||
|
||||
if (!patternCooldownTracker.TryGetValue(pattern, out float readyTime))
|
||||
return true;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user