Project_WL/Assets/WL/Combat/AttackStepDriver.cs

170 lines
7.8 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// AttackStepDriver.cs — 공격 모션에 맞춘 전진 이동 (PD 지시 #762)
//
// 클립의 `Move(float)` 애니메이션 이벤트가 들어오면 그 시점부터
// attackStepSeconds 동안 캐릭터 전방으로 (float × attackStepScale) m 를
// **NavMeshAgent.Move** 로 나눠서 밀어 준다.
//
// ── 왜 NavMeshAgent.Move 인가 ───────────────────────────────────────────────
// · transform.position 을 직접 더하면 지형·장애물을 통과하고 에이전트 내부 위치와
// 어긋나 다음 이동 명령에서 순간이동한다.
// · NavMeshAgent.Move 는 이동을 NavMesh 표면에 투영하고 에이전트 내부 상태도 함께
// 갱신한다 → 지형 관통이 없고 이후 조이스틱 이동과도 충돌하지 않는다.
// · CharacterController 는 이 프로젝트에 없다(Actor 는 NavMeshAgent 만 쓴다).
//
// ── 취소 조건 ───────────────────────────────────────────────────────────────
// 사망 · CC(피격/경직/넉백) · 정지(IsStop) · 공격 상태 이탈(회피·피격으로 전환)
// → PCActor.WL_CanAttackStep() 한 곳에서 판정한다(IsStop 이 protected 라 Actor 밖에서 못 읽는다).
//
// 대상은 PC 뿐이다. 펫·몬스터는 PCActor 가 아니므로 이 경로를 타지 않는다.
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
using UnityEngine.AI;
namespace WL.Combat
{
/// <summary>
/// 공격 클립의 Move 이벤트를 받아 캐릭터를 전방으로 조금씩 밀어 준다.
/// 필요할 때 런타임으로 자동 부착되므로 프리팹을 미리 손댈 필요가 없다.
/// </summary>
[DisallowMultipleComponent]
public sealed class AttackStepDriver : MonoBehaviour
{
private PCActor _pc;
private NavMeshAgent _agent;
private WLCombatMotionSettings _cfg;
private bool _active;
private float _elapsed;
private float _duration;
private float _total; // 이번 스텝의 총 이동 거리(m)
private float _moved; // 지금까지 실제로 넣은 이동량(m · 계획값)
private Vector3 _dir;
private Vector3 _startPos;
// ── 검증용 (Play 중 CLI 로 읽는다 · 게임 로직은 쓰지 않는다) ────────────
/// <summary>마지막 전진의 계획 거리(m).</summary>
public static float LastPlannedDistance;
/// <summary>마지막 전진의 실제 이동 거리(m · NavMesh 투영 후).</summary>
public static float LastActualDistance;
/// <summary>지금까지 전진을 시작한 횟수.</summary>
public static int StepCount;
/// <summary>마지막 전진 요약(검증용 문자열).</summary>
public static string LastInfo;
/// <summary>전진 중인가.</summary>
public bool IsStepping { get { return _active; } }
/// <summary>
/// 전진을 시작한다. 대상이 PC 가 아니거나 설정이 없으면 아무 일도 하지 않는다.
/// <paramref name="b"/> = 클립 Move 이벤트의 float 파라미터(전진 거리 m).
/// </summary>
public static bool Begin(PCActor pc, float b)
{
if (pc == null || b <= 0f) return false;
var cfg = WLCombatMotionSettings.Instance;
if (cfg == null || !cfg.enableAttackStep) return false;
if (cfg.attackMoveMode != AttackMoveMode.FixedStep) return false; // PD #789/#791: 고정 전진은 FixedStep 모드에서만 (FrameTable/ClipRootMotion 과 이중 적용 방지)
var agent = pc.Get_Agent();
if (agent == null || !agent.enabled || !agent.isOnNavMesh) return false;
var driver = pc.GetComponent<AttackStepDriver>();
if (driver == null) driver = pc.gameObject.AddComponent<AttackStepDriver>();
return driver.BeginInternal(pc, agent, cfg, b);
}
private bool BeginInternal(PCActor pc, NavMeshAgent agent, WLCombatMotionSettings cfg, float b)
{
_pc = pc;
_agent = agent;
_cfg = cfg;
float dist = Mathf.Min(b * cfg.attackStepScale, Mathf.Max(cfg.attackStepMaxDistance, 0f));
if (dist <= 0.0001f) return false;
_dir = transform.forward;
_dir.y = 0f;
if (_dir.sqrMagnitude < 1e-6f) return false;
_dir.Normalize();
_total = dist;
_moved = 0f;
_elapsed = 0f;
_duration = Mathf.Max(cfg.attackStepSeconds, 0.01f);
_startPos = transform.position;
_active = true;
StepCount++;
LastPlannedDistance = dist;
#if UNITY_EDITOR
if (cfg.verboseLog)
Debug.Log(string.Format("[WL #762] 전진 시작 b={0:F2} → {1:F2}m / {2:F2}s anim={3}",
b, dist, _duration, pc.Get_CurAnim()), this);
#endif
return true;
}
/// <summary>전진을 즉시 멈춘다(피격·회피·사망).</summary>
public void Cancel()
{
if (!_active) return;
_active = false;
LastActualDistance = Vector3.Distance(_startPos, transform.position);
LastInfo = string.Format("cancel planned={0:F2} actual={1:F2}", _total, LastActualDistance);
}
private void Update()
{
if (!_active) return;
if (_pc == null || _agent == null || !_agent.enabled || !_agent.isOnNavMesh || !_pc.WL_CanAttackStep())
{
Cancel();
return;
}
// 대상을 통과해 지나가지 않도록, 이미 붙어 있으면 더 밀지 않는다.
if (_cfg != null && _cfg.attackStepStopDistance > 0f)
{
var target = _pc.Get_Target();
if (target != null && !target.IsDead())
{
Vector3 d = target.Get_position() - transform.position;
d.y = 0f;
if (d.magnitude <= _cfg.attackStepStopDistance) { Cancel(); return; }
}
}
_elapsed += Time.deltaTime;
float p = Mathf.Clamp01(_elapsed / _duration);
// ease-out: 앞이 빠르고 뒤가 느리다. 실측 루트 커브가 초반에 몰려 있는 형태와 맞는다.
float eased = (_cfg != null && _cfg.attackStepEaseOut) ? 1f - (1f - p) * (1f - p) : p;
float want = _total * eased - _moved;
if (want > 0f)
{
_moved += want;
_agent.Move(_dir * want); // NavMesh 표면에 투영된다 — 지형 관통 없음
}
if (p >= 1f)
{
_active = false;
LastActualDistance = Vector3.Distance(_startPos, transform.position);
LastInfo = string.Format("done planned={0:F2} actual={1:F2}", _total, LastActualDistance);
#if UNITY_EDITOR
if (_cfg != null && _cfg.verboseLog) Debug.Log("[WL #762] 전진 종료 " + LastInfo, this);
#endif
}
}
private void OnDisable() { Cancel(); }
}
}