143 lines
8.2 KiB
C#
143 lines
8.2 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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(); }
|
||
}
|
||
}
|