Project_WL/Assets/WL/Combat/SlashArcFollower.cs

143 lines
8.2 KiB
C#
Raw 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.

// ─────────────────────────────────────────────────────────────────────────────
// SlashArcFollower.cs — 슬래시 궤적 이펙트가 플레이어를 따라가게 한다 (PD 지시 #777)
//
// PD 원문 (2026-09-06):
// "무기 궤적 이펙트가 플레이어 움직임에 맞게 위치가 자연스럽게 이동되어야하는데
// 지금은 고정 된 위치에 나와서 이펙트와 무기가 떨어진 느낌이야. 수정해줘"
//
// ── 왜 떨어져 보이는가 (구조) ───────────────────────────────────────────────
// #761 BurstMode.PrefabArc 는 스윙이 끝난 순간(OnSwingEnd) 검끝 궤적으로 평면을 구해
// 원본 슬래시 프리팹을 **월드 고정**으로 1회 배치한다(부모 없음). 배치 뒤에는 아무도
// 그 인스턴스를 움직이지 않는다.
// 거기에 #762 AttackStepDriver 가 공격마다 캐릭터를 0.30/0.70/0.65 m 전진시키므로
// (0.25 s 동안 NavMeshAgent.Move) **이펙트는 스윙 지점에 남고 캐릭터만 앞으로 나간다.**
// → 이펙트 수명(0.4~1.0 s) 동안 그 거리만큼 무기와 이펙트가 벌어진다.
//
// ── 이 컴포넌트가 하는 일 ───────────────────────────────────────────────────
// 스폰 콜백에서 인스턴스에 붙어, 매 LateUpdate 마다 **스폰 시점 PC 기준 상대 오프셋**을
// 유지하도록 위치를 갱신한다. 회전 추종은 선택(arcFollowRotation).
// · 위치만 추종 : pos = anchor.position + (스폰 시점 월드 오프셋)
// · 회전까지 추종: pos/rot = anchor 로컬 좌표계에 고정 (PC 가 돌면 이펙트도 돈다)
//
// ── 왜 부모로 붙이지 않는가 (설계 결정) ─────────────────────────────────────
// InGameInfo 는 이펙트 인스턴스를 풀(dic_str_Effect)에서 재사용하고 TurnOff_GO 가
// 수명이 끝나면 SetActive(false) 로 돌려놓는다. 이때 부모가 PC 로 바뀌어 있으면
// · PC 가 파괴/비활성될 때 풀 인스턴스가 같이 사라지거나 비활성 트리에 갇히고
// · 다음 재사용 때 부모가 남아 있어 엉뚱한 곳에서 재생된다.
// 풀 구조를 건드리지 않는 것이 조건이므로(발주 A2) **부모는 그대로 두고 좌표만 따라간다**.
// 기각안: transform.SetParent(pc) — 위 이유로 기각. 원복도 어렵다.
//
// ── 파티클 시뮬레이션 공간 ──────────────────────────────────────────────────
// 이미터가 Local(moveWithTransform=0)이면 이미 방출된 입자도 루트를 따라오므로 잔상이 없다.
// World 인 이미터가 섞여 있으면 그 입자만 제자리에 남는다 → 실측해 보고한다(발주 A2).
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat
{
/// <summary>
/// 스폰된 슬래시 이펙트 인스턴스가 플레이어를 따라 움직이게 한다.
/// 런타임에만 AddComponent 로 붙으므로 프리팹 에셋은 건드리지 않는다(즉시 원복 · C8).
/// </summary>
[DisallowMultipleComponent]
public sealed class SlashArcFollower : MonoBehaviour
{
private Transform _anchor;
private bool _followRotation;
private float _expireTime; // Time.time 기준. 0 이하 = 이펙트 수명 전체
private bool _active;
private Vector3 _worldOffset; // 위치 추종 — 스폰 시점 (이펙트 PC) 월드 오프셋
private Vector3 _localPos; // 회전 추종 — PC 로컬 위치
private Quaternion _localRot; // 회전 추종 — PC 로컬 회전
// ── 검증용 (Play 중 CLI 로 읽는다 · 게임 로직은 쓰지 않는다) ────────────
/// <summary>지금까지 추종을 시작한 횟수.</summary>
public static int AttachCount;
/// <summary>마지막으로 추종을 시작한 인스턴스.</summary>
public static SlashArcFollower Last;
/// <summary>마지막 추종 시작 요약.</summary>
public static string LastInfo;
/// <summary>추종 중인가.</summary>
public bool IsFollowing { get { return _active && _anchor != null; } }
/// <summary>스폰 시점 (이펙트 PC) 월드 오프셋.</summary>
public Vector3 SpawnOffset { get { return _worldOffset; } }
/// <summary>추종 기준(PC) 트랜스폼. 해제됐으면 null.</summary>
public Transform Anchor { get { return _anchor; } }
/// <summary>
/// 인스턴스에 추종을 건다. 이미 붙어 있으면 기준을 다시 잡는다(풀 재사용 대응).
/// </summary>
/// <param name="go">스폰된 이펙트 인스턴스</param>
/// <param name="anchor">따라갈 기준(PC 루트 트랜스폼)</param>
/// <param name="followRotation">PC 회전까지 따라갈지</param>
/// <param name="seconds">추종 유지 시간(초). 0 이하 = 이펙트 수명 전체</param>
public static SlashArcFollower Attach(GameObject go, Transform anchor, bool followRotation, float seconds)
{
if (go == null || anchor == null) return null;
var f = go.GetComponent<SlashArcFollower>();
if (f == null) f = go.AddComponent<SlashArcFollower>();
f.Begin(anchor, followRotation, seconds);
return f;
}
/// <summary>추종을 끈다(설정에서 arcFollowPlayer 를 껐을 때 · 풀 인스턴스 원복).</summary>
public static void Detach(GameObject go)
{
if (go == null) return;
var f = go.GetComponent<SlashArcFollower>();
if (f != null) f.Release();
}
private void Begin(Transform anchor, bool followRotation, float seconds)
{
_anchor = anchor;
_followRotation = followRotation;
_expireTime = seconds > 0f ? Time.time + seconds : 0f;
_active = true;
_worldOffset = transform.position - anchor.position;
_localPos = anchor.InverseTransformPoint(transform.position);
_localRot = Quaternion.Inverse(anchor.rotation) * transform.rotation;
AttachCount++;
Last = this;
LastInfo = string.Format("{0} anchor={1} offset={2} rot={3} sec={4:F2}",
gameObject.name, anchor.name, _worldOffset.ToString("F2"),
followRotation, seconds);
}
/// <summary>추종을 즉시 해제한다. 위치는 그 자리에 남는다(기존 동작과 같아진다).</summary>
public void Release()
{
_active = false;
_anchor = null;
}
private void LateUpdate()
{
if (!_active) return;
if (_anchor == null) { _active = false; return; }
if (_expireTime > 0f && Time.time >= _expireTime) { _active = false; return; }
if (_followRotation)
{
transform.position = _anchor.TransformPoint(_localPos);
transform.rotation = _anchor.rotation * _localRot;
}
else
{
transform.position = _anchor.position + _worldOffset;
}
}
// 풀에 반환될 때(SetActive(false)) 반드시 끊는다 — 다음 재사용에서 이전 PC 를
// 계속 따라다니는 일이 없도록. 다시 쓸 때는 스폰 콜백이 Attach 로 새로 건다.
private void OnDisable() { Release(); }
}
}