feat: 방어 시스템과 드로그 검증 경로 정리
- 애니메이션 이벤트 기반 방어/유지/해제 흐름과 HUD 피드백, 방어 디버그 로그를 추가했다. - 드로그 기본기1 테스트 패턴을 정리하고 공격 판정을 OnEffect 기반으로 옮기며 드로그 범위 효과의 타겟 레이어를 정상화했다. - 플레이어 퀵슬롯 테스트 세팅과 적-플레이어 겹침 방지 로직을 조정해 충돌 시 적이 수평 이동을 멈추고 최소 분리만 수행하게 했다.
This commit is contained in:
@@ -9,6 +9,7 @@ using Colosseum.Abnormalities;
|
||||
using Colosseum.Passives;
|
||||
using Colosseum.Stats;
|
||||
using Colosseum.Combat;
|
||||
using Colosseum.Player;
|
||||
using Colosseum.Skills;
|
||||
|
||||
namespace Colosseum.Enemy
|
||||
@@ -28,6 +29,8 @@ namespace Colosseum.Enemy
|
||||
[SerializeField] protected UnityEngine.AI.NavMeshAgent navMeshAgent;
|
||||
[Tooltip("이상상태 관리자")]
|
||||
[SerializeField] protected AbnormalityManager abnormalityManager;
|
||||
[Tooltip("플레이어와의 물리 겹침을 계산할 본체 콜라이더")]
|
||||
[SerializeField] private Collider bodyCollider;
|
||||
|
||||
[Header("Data")]
|
||||
[SerializeField] protected EnemyData enemyData;
|
||||
@@ -46,6 +49,12 @@ namespace Colosseum.Enemy
|
||||
[Tooltip("보호막 타입이 지정되지 않았을 때 사용할 기본 보호막 이상상태 데이터")]
|
||||
[SerializeField] private AbnormalityData shieldStateAbnormality;
|
||||
|
||||
[Header("Player Separation")]
|
||||
[Tooltip("적과 플레이어 사이에 추가로 유지할 수평 간격")]
|
||||
[Min(0f)] [SerializeField] private float playerSeparationPadding = 0.1f;
|
||||
[Tooltip("플레이어와 닿아 있을 때 적의 수평 이동을 멈출지 여부")]
|
||||
[SerializeField] private bool freezeHorizontalMotionOnPlayerContact = true;
|
||||
|
||||
|
||||
// 네트워크 동기화 변수
|
||||
protected NetworkVariable<float> currentHealth = new NetworkVariable<float>(100f);
|
||||
@@ -95,6 +104,8 @@ namespace Colosseum.Enemy
|
||||
navMeshAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
|
||||
if (abnormalityManager == null)
|
||||
abnormalityManager = GetComponent<AbnormalityManager>();
|
||||
if (bodyCollider == null)
|
||||
bodyCollider = GetComponent<Collider>();
|
||||
|
||||
// 서버에서 초기화
|
||||
if (IsServer)
|
||||
@@ -126,34 +137,27 @@ namespace Colosseum.Enemy
|
||||
protected virtual void OnServerUpdate() { }
|
||||
|
||||
/// <summary>
|
||||
/// 보스와 플레이어가 겹치면 플레이어를 밀어냅니다.
|
||||
/// 보스와 플레이어가 겹치면 적 자신을 살짝 밀어내 겹침을 해소합니다.
|
||||
/// 점프 착지 포함, 항상 실행됩니다.
|
||||
/// </summary>
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (!IsServer || IsDead) return;
|
||||
|
||||
float separationDist = navMeshAgent != null
|
||||
? Mathf.Max(navMeshAgent.stoppingDistance, navMeshAgent.radius + 0.5f)
|
||||
: 1f;
|
||||
Vector3 separationOffset = ComputePlayerSeparationOffset();
|
||||
if (separationOffset.sqrMagnitude <= 0.000001f)
|
||||
return;
|
||||
|
||||
int count = Physics.OverlapSphereNonAlloc(transform.position, separationDist, overlapBuffer);
|
||||
for (int i = 0; i < count; i++)
|
||||
if (navMeshAgent != null && !isAirborne && navMeshAgent.enabled)
|
||||
{
|
||||
if (!overlapBuffer[i].TryGetComponent<CharacterController>(out var cc)) continue;
|
||||
|
||||
Vector3 toPlayer = overlapBuffer[i].transform.position - transform.position;
|
||||
toPlayer.y = 0f;
|
||||
float dist = toPlayer.magnitude;
|
||||
if (dist >= separationDist) continue;
|
||||
|
||||
// 플레이어를 보스 바깥으로 밀어냄
|
||||
Vector3 pushDir = dist > 0.001f ? toPlayer.normalized : transform.forward;
|
||||
cc.Move(pushDir * (separationDist - dist));
|
||||
|
||||
// 보스가 이동 중이었으면 정지 (플레이어 안으로 더 진입하지 않도록)
|
||||
if (navMeshAgent != null && !isAirborne && navMeshAgent.velocity.sqrMagnitude > 0.01f)
|
||||
if (navMeshAgent.velocity.sqrMagnitude > 0.01f)
|
||||
navMeshAgent.isStopped = true;
|
||||
|
||||
navMeshAgent.Move(separationOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
transform.position += separationOffset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,23 +226,10 @@ namespace Colosseum.Enemy
|
||||
navMeshAgent.Warp(transform.position);
|
||||
}
|
||||
|
||||
// XZ 차단: 플레이어 방향으로의 이동 방지 (일반 이동 중에만)
|
||||
float blockRadius = Mathf.Max(navMeshAgent.stoppingDistance, navMeshAgent.radius + 0.5f);
|
||||
int count = Physics.OverlapSphereNonAlloc(transform.position, blockRadius, overlapBuffer);
|
||||
for (int i = 0; i < count; i++)
|
||||
if (freezeHorizontalMotionOnPlayerContact && IsTouchingPlayer())
|
||||
{
|
||||
if (!overlapBuffer[i].TryGetComponent<CharacterController>(out _)) continue;
|
||||
|
||||
Vector3 toPlayer = overlapBuffer[i].transform.position - transform.position;
|
||||
toPlayer.y = 0f;
|
||||
if (toPlayer.sqrMagnitude < 0.0001f) continue;
|
||||
|
||||
Vector3 deltaXZ = new Vector3(deltaPosition.x, 0f, deltaPosition.z);
|
||||
if (Vector3.Dot(deltaXZ, toPlayer.normalized) > 0f)
|
||||
{
|
||||
deltaPosition.x = 0f;
|
||||
deltaPosition.z = 0f;
|
||||
}
|
||||
deltaPosition.x = 0f;
|
||||
deltaPosition.z = 0f;
|
||||
}
|
||||
|
||||
navMeshAgent.Move(deltaPosition);
|
||||
@@ -282,23 +273,36 @@ namespace Colosseum.Enemy
|
||||
/// 대미지 적용 (서버에서 실행)
|
||||
/// </summary>
|
||||
public virtual float TakeDamage(float damage, object source = null)
|
||||
{
|
||||
return TakeDamage(new DamageContext(damage, source));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 대미지 컨텍스트를 사용해 대미지를 적용합니다.
|
||||
/// </summary>
|
||||
public virtual float TakeDamage(DamageContext damageContext)
|
||||
{
|
||||
if (!IsServer || isDead.Value)
|
||||
return 0f;
|
||||
|
||||
if (ShouldIgnoreIncomingDamage(damage, source))
|
||||
float damage = damageContext.Amount;
|
||||
if (damage <= 0f)
|
||||
return 0f;
|
||||
|
||||
if (ShouldIgnoreIncomingDamage(damage, damageContext.Source))
|
||||
return 0f;
|
||||
|
||||
float mitigatedDamage = ConsumeShield(damage);
|
||||
float actualDamage = Mathf.Min(mitigatedDamage, currentHealth.Value);
|
||||
currentHealth.Value = Mathf.Max(0f, currentHealth.Value - actualDamage);
|
||||
|
||||
CombatBalanceTracker.RecordDamage(source as GameObject, gameObject, actualDamage);
|
||||
RegisterThreatFromDamage(actualDamage, source);
|
||||
GameObject sourceObject = damageContext.SourceGameObject;
|
||||
CombatBalanceTracker.RecordDamage(sourceObject, gameObject, actualDamage);
|
||||
RegisterThreatFromDamage(actualDamage, sourceObject);
|
||||
OnDamageTaken?.Invoke(actualDamage);
|
||||
|
||||
// 대미지 피드백 (애니메이션, 이펙트 등)
|
||||
OnTakeDamageFeedback(actualDamage, source);
|
||||
OnTakeDamageFeedback(actualDamage, damageContext.Source);
|
||||
|
||||
if (currentHealth.Value <= 0f)
|
||||
{
|
||||
@@ -327,6 +331,93 @@ namespace Colosseum.Enemy
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3 ComputePlayerSeparationOffset()
|
||||
{
|
||||
if (bodyCollider == null)
|
||||
return Vector3.zero;
|
||||
|
||||
float scanRadius = GetPlayerDetectionRadius();
|
||||
int count = Physics.OverlapSphereNonAlloc(transform.position, scanRadius, overlapBuffer);
|
||||
Vector3 separationOffset = Vector3.zero;
|
||||
int overlapCount = 0;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (!TryGetPlayerCharacterController(overlapBuffer[i], out CharacterController playerController))
|
||||
continue;
|
||||
if (!Physics.ComputePenetration(
|
||||
bodyCollider, transform.position, transform.rotation,
|
||||
playerController, playerController.transform.position, playerController.transform.rotation,
|
||||
out Vector3 separationDirection, out float separationDistance))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
separationDirection.y = 0f;
|
||||
if (separationDirection.sqrMagnitude <= 0.0001f)
|
||||
separationDirection = -transform.forward;
|
||||
|
||||
separationOffset += separationDirection.normalized * (separationDistance + playerSeparationPadding);
|
||||
overlapCount++;
|
||||
}
|
||||
|
||||
if (overlapCount <= 0)
|
||||
return Vector3.zero;
|
||||
|
||||
separationOffset /= overlapCount;
|
||||
separationOffset.y = 0f;
|
||||
return separationOffset;
|
||||
}
|
||||
|
||||
private bool IsTouchingPlayer()
|
||||
{
|
||||
float scanRadius = GetPlayerDetectionRadius();
|
||||
int count = Physics.OverlapSphereNonAlloc(transform.position, scanRadius, overlapBuffer);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (!TryGetPlayerCharacterController(overlapBuffer[i], out CharacterController playerController))
|
||||
continue;
|
||||
|
||||
Vector3 toPlayer = playerController.transform.position - transform.position;
|
||||
toPlayer.y = 0f;
|
||||
if (toPlayer.magnitude < GetRequiredSeparationDistance(playerController))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private float GetPlayerDetectionRadius()
|
||||
{
|
||||
float enemyRadius = navMeshAgent != null ? navMeshAgent.radius : 0.5f;
|
||||
return enemyRadius + 1f + playerSeparationPadding;
|
||||
}
|
||||
|
||||
private float GetRequiredSeparationDistance(CharacterController playerController)
|
||||
{
|
||||
float enemyRadius = navMeshAgent != null ? navMeshAgent.radius : 0.5f;
|
||||
float playerRadius = playerController != null ? playerController.radius : 0.5f;
|
||||
return enemyRadius + playerRadius + playerSeparationPadding;
|
||||
}
|
||||
|
||||
private static bool TryGetPlayerCharacterController(Collider overlapCollider, out CharacterController playerController)
|
||||
{
|
||||
playerController = null;
|
||||
if (overlapCollider == null)
|
||||
return false;
|
||||
|
||||
playerController = overlapCollider.GetComponent<CharacterController>();
|
||||
if (playerController == null)
|
||||
playerController = overlapCollider.GetComponentInParent<CharacterController>();
|
||||
|
||||
if (playerController == null)
|
||||
return false;
|
||||
|
||||
return playerController.GetComponent<PlayerNetworkController>() != null
|
||||
|| playerController.GetComponentInParent<PlayerNetworkController>() != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 체력 회복 (서버에서 실행)
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user