Project_WL/Assets/WL/Combat/SlashArcFollower.cs

143 lines
8.2 KiB
C#
Raw Normal View History

WL 리뉴얼 세션 2026-09-06~07 (#760~#800) — 전투·배경·UI·타격감 (조직 PD 로그 #746~#800 · 대화로그 §14~§24) - 전투: Knight@Attack1~3_S 3콤보(4 컨트롤러·클래스별 서브클립) · 타겟팅 4규칙(정면 우선·재타겟·즉시 공격·어그로 금지 · WLTargetingSettings) · 대쉬 후 공격(Stander@Chase_Start · DashDriver · 5 m · 사정거리+0.8 · 근거리 적 우선) · 공격 이동 FrameTable(발 접지 실측 표 · AttackRootMotion) · 충돌 반경/투사체 0.3 배(WLCollisionTuning) · 무적(임시)·펫 금지(WLGameplaySettings) - 검기: NamuFX Slash_B 배리언트 Effect_WLSwingArc 원 피팅 정합 배치 + 캘리브레이션(SlashArcMeasure · SlashTrailSettings) · 찌르기 Effect_WLStab 대기 · 램프 리본(BladeTrail · WL_BladeRibbon.shader · T_WL_BladeRibbonRamp) 보존(drawRibbon 0) - 타격감: Assets/WL/Feel(WLHitFeel · 히트스톱 0.03 · 셰이크 0.10 m · 몹 펀치 1.12 · Actor.Get_Damage 훅 1줄 · 원본 RealCamera 셰이크 결함 대체) - 배경/맵: LMHPOLY Demo_01~10 → WL_Nature01~10(프리팹·씬·NavMesh·스포너·BattleMapConfig) · 물(ToonWaterU) · 포스트 블룸 0.9/0.3 · 잔디(BruteForce·드레싱) 제거 · 마젠타 머티리얼 URP 변환 - UI: 세로 HUD(WL_HUD · 하단 5메뉴 폭 전체 · 채팅/전투 패드 숨김 · WLIngameUiOverride) · Title/TitleInfo 1080×1920 Expand + 배경 높이 fit(WLBackgroundFit) · 로딩 SortOrder_5(WLRawImageAspectSync · 초점표 8장) · Loading1~8 ASTC 6×6 - 도구: AgentScripts/*(LightProbe · WL_MapSwitch · WL760~WL800 프로브/집행/검증 · 상단 사용법 주석) · 에디터 락 프로토콜 파일(staging) - 제외(별도 커밋 예정): Assets/LMHPOLY(703 MB) · Assets/Feel(422 MB) · Assets/Shinabro(300 MB) — 에셋 스토어/구 WL 팩 원본 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:24:25 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// 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(); }
}
}