Project_WL/AgentScripts/drafts/WL797/WLHitFeelCameraShake.cs

124 lines
5.7 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.

using UnityEngine;
/// <summary>
/// PD #797 — 타격 카메라 셰이크. RealCamera 의 보간 루프 **바깥**에서 오프셋을 가감산한다.
///
/// ■ 왜 그냥 더하면 안 되나 (CameraMoveLead.cs 주석의 기각 근거와 같은 이유)
/// RealCamera.Update_Cam() 은
/// transform.position = Vector3.Lerp(transform.position, camPosition, Time.deltaTime * smooth)
/// 로 **자기 출력을 다시 입력으로 읽는** 저역통과 필터다(실측 smooth = 4 → a = dt·smooth = 0.0667).
/// 여기에 매 프레임 오프셋을 사후 가산하면 평형점이 camPosition + ((2a1)/a)·e 로 어긋난다
/// (계수 13.0 · 프레임률에 따라 또 변함). 즉 의도의 13 배가 반대 방향으로 나온다.
///
/// ■ 기존 RealCamera.ShakeCamera / Shaking() 이 실제로 하는 일 (2026-09-07 코드 실측)
/// LateUpdate() 첫 줄에서 Shaking() 이 먼저 돌아
/// transform.localPosition = originalPosition + shakeOffset
/// 을 쓰고, **그 뒤** 같은 LateUpdate 안에서 Update_Cam() 의 Lerp 가 그 값을 입력으로 읽는다.
/// 게다가 originalPosition 은 셰이크 **시작 시점**에 한 번 잡고 갱신하지 않는다.
/// → 셰이크가 걸린 0.25 s 동안 카메라가 '셰이크 시작 위치' 에 93 % 붙들리고,
/// 그 사이 달아난 플레이어를 StopShake 이후에야 따라잡는다.
/// 이 컴포넌트는 그 경로를 대체한다(WLHitFeelSettings.suppressLegacyShake).
///
/// ■ 해결 — 필터 상태를 오염시키지 않는 순수 표시용 오프셋
/// Update() : 지난 프레임에 더해 둔 오프셋을 정확히 뺀다 → 필터는 원래 상태로 돌아간다.
/// (모든 Update 는 모든 LateUpdate 보다 먼저 돈다 = RealCamera 보다 먼저다)
/// LateUpdate() : DefaultExecutionOrder 200 으로 RealCamera(기본 0) 의 LateUpdate 뒤에 돌아
/// 새 오프셋을 더한다.
/// → RealCamera 의 Lerp 는 셰이크를 한 번도 보지 못한다. 카메라 리드(CameraMoveLead)와도 싸우지 않는다.
///
/// ■ 안전장치
/// · 우리가 쓴 위치와 현재 위치가 다르면(워프·MoveCam_TargetBack 등 외부가 카메라를 옮겼다면)
/// 빼기를 포기한다 → 잘못된 좌표를 만들지 않는다. 최대 잔차는 진폭 1 회분(≤0.12 m).
/// · unscaled 시간으로 진행하므로 히트스톱(timeScale 0) 중에도 셰이크가 살아 있다.
/// · 비활성/파괴 시 오프셋을 원복한다.
/// </summary>
[DefaultExecutionOrder(200)]
public class WLHitFeelCameraShake : MonoBehaviour
{
Vector3 _applied; // 지난 프레임에 더해 둔 오프셋(월드)
Vector3 _writtenPos; // 그때 우리가 써 놓은 최종 위치 — 외부 개입 감지용
bool _hasApplied;
float _timer, _duration, _amplitude, _frequency;
Vector3 _axisWeight = Vector3.one;
float _seedX, _seedY, _seedZ;
/// <summary>진단 — 이번 세션에서 관측한 오프셋 크기 최대치(m).</summary>
public float MaxOffsetMagnitude;
/// <summary>진단 — 현재 프레임에 적용 중인 오프셋(월드).</summary>
public Vector3 CurrentOffset { get { return _hasApplied ? _applied : Vector3.zero; } }
public bool IsShaking { get { return _timer > 0f; } }
/// <summary>셰이크 시작. 진행 중이면 더 강한 쪽으로 덮어쓴다(중첩 금지 = '과하지 않게').</summary>
public void Shake(float amplitude, float duration, float frequency, Vector3 axisWeight)
{
if (amplitude <= 0f || duration <= 0f) return;
// 진행 중인 셰이크가 더 강하면 무시 — 군집 타격에서 진폭이 누적되지 않게 한다.
if (_timer > 0f && amplitude < _amplitude * (_timer / Mathf.Max(_duration, 0.0001f))) return;
_amplitude = amplitude;
_duration = Mathf.Max(0.01f, duration);
_frequency = Mathf.Max(1f, frequency);
_axisWeight = axisWeight;
_timer = _duration;
_seedX = Random.value * 128f;
_seedY = Random.value * 128f;
_seedZ = Random.value * 128f;
}
public void StopShake()
{
_timer = 0f;
Revert();
}
void Update()
{
// RealCamera.LateUpdate 보다 먼저 — 필터 입력을 원래 상태로 되돌린다.
Revert();
}
void LateUpdate()
{
// RealCamera.LateUpdate(실행 순서 0) 가 끝난 뒤 — 표시용 오프셋만 더한다.
if (_timer <= 0f) return;
_timer -= Time.unscaledDeltaTime;
if (_timer <= 0f) { _timer = 0f; return; }
float remain01 = _timer / _duration; // 1 → 0
float damper = remain01 * remain01; // 끝을 부드럽게 감쇠
float ph = Time.unscaledTime * _frequency;
Vector3 local = new Vector3(
(Mathf.PerlinNoise(_seedX, ph) - 0.5f) * 2f * _axisWeight.x,
(Mathf.PerlinNoise(_seedY, ph) - 0.5f) * 2f * _axisWeight.y,
(Mathf.PerlinNoise(_seedZ, ph) - 0.5f) * 2f * _axisWeight.z);
Vector3 offset = transform.TransformDirection(local) * (_amplitude * damper);
transform.position += offset;
_applied = offset;
_writtenPos = transform.position;
_hasApplied = true;
float m = offset.magnitude;
if (m > MaxOffsetMagnitude) MaxOffsetMagnitude = m;
}
void Revert()
{
if (!_hasApplied) return;
// 그 사이 외부(워프·MoveCam_TargetBack·컷신)가 카메라를 옮겼다면 빼기를 포기한다.
if ((transform.position - _writtenPos).sqrMagnitude < 1e-6f)
transform.position -= _applied;
_applied = Vector3.zero;
_hasApplied = false;
}
void OnDisable() { _timer = 0f; Revert(); }
}