CapersProject/Assets/0_Voyage/_Scripts/Ship/VoyagePlayerShipMovementVisual.cs
2025-07-25 13:51:43 +09:00

264 lines
9.1 KiB
C#

using System;
using UnityEngine;
using Random = UnityEngine.Random;
[RequireComponent(typeof(VoyagePlayerShipMovement))]
public class VoyagePlayerShipMovementVisual : MonoBehaviour
{
[Header("메시 설정")]
[SerializeField] private GameObject visualGameObject;
public GameObject VisualGameObject => visualGameObject;
[Header("비주얼 메시 회전 설정")]
[SerializeField] private float angularVelocityToYaw = 8;
[SerializeField] private bool overrideMeshYaw = true;
[SerializeField] private float yawRotationSmoothTime = 0.3f;
[Header("회전 틸트 설정")]
[SerializeField] private float maxRotationTiltAngle = 5f;
[SerializeField] private float rotationTiltSpeed = 5f;
[SerializeField] private float rotationTiltReturnSpeed = 3f;
[SerializeField] private float angularVelocityMultiplier = 2f;
[Header("가속 틸트 설정")]
[SerializeField] private float maxAccelTiltAngle = 10;
[SerializeField] private float accelTiltForce = 15f;
[SerializeField] private float accelTiltDamping = 0.9f;
[SerializeField] private float accelTiltSpeed = 10f;
[SerializeField] private float springStiffness = 30f;
[SerializeField] private float springDamping = 15f;
[Header("파도 효과 설정")]
[SerializeField] private float minSpeedWaveHeight = 0.1f;
public float MinSpeedWaveHeight => minSpeedWaveHeight;
[SerializeField] private float maxSpeedWaveHeight = 0.05f;
public float MaxSpeedWaveHeight => maxSpeedWaveHeight;
[SerializeField] private float baseWaveFrequency = 1f;
public float BaseWaveFrequency => baseWaveFrequency;
[SerializeField] private float speedWaveMultiplier = 5f;
[SerializeField] private float randomWaveOffset = 0.5f;
[SerializeField] private float waveUnitSpeed = 10f;
public float WaveUnitSpeed => waveUnitSpeed;
private VoyagePlayerShipMovement movement;
private Quaternion originalMeshRotation;
private Vector3 originalMeshPosition;
// 회전 관련 변수
private float desiredWorldYaw = 0f;
private float targetYaw = 0f;
// 틸트 관련 변수들
private float currentRotationTilt;
private float lastRotationY;
private float currentAngularVelocity;
private float currentAccelTilt;
private float accelTiltVelocity;
private float prevSpeed;
// 파도 관련 변수들
private float waveTime;
public float WaveTime => waveTime;
private float waveRandomOffset;
private float currentWaveHeight;
private Camera mainCamera;
public float CurrentWaveHeight => currentWaveHeight;
private void Start()
{
mainCamera = Camera.main;
InitializeComponents();
InitializeMeshTransform();
InitializeWaveEffect();
}
#region Initialization
private void InitializeWaveEffect()
{
waveTime = 0f;
waveRandomOffset = Random.Range(-randomWaveOffset, randomWaveOffset);
}
private void ValidateMeshTransform()
{
if (Application.isEditor && !Application.isPlaying && visualGameObject is null)
{
Debug.LogWarning("Mesh Transform을 Inspector에서 할당해주세요.");
}
}
private void InitializeMeshTransform()
{
if (visualGameObject is null)
{
Debug.LogError("Mesh Transform이 할당되지 않았습니다.");
enabled = false;
return;
}
originalMeshPosition = visualGameObject.transform.localPosition;
originalMeshRotation = visualGameObject.transform.localRotation;
lastRotationY = transform.eulerAngles.y;
}
private void InitializeComponents()
{
movement = GetComponent<VoyagePlayerShipMovement>();
if (visualGameObject == null)
{
Debug.LogError("Mesh Transform이 할당되지 않았습니다.");
enabled = false;
return;
}
originalMeshPosition = visualGameObject.transform.localPosition;
originalMeshRotation = visualGameObject.transform.localRotation;
lastRotationY = transform.eulerAngles.y;
waveTime = 0f;
waveRandomOffset = Random.Range(-randomWaveOffset, randomWaveOffset);
}
#endregion
private void FixedUpdate()
{
if (visualGameObject is null) return;
UpdateMeshRotationToCamera();
UpdateMeshRotationTilt();
UpdateAccelerationTilt();
ApplyMeshTilt();
UpdateWaveMotion();
ApplyMeshOffset();
}
private void OnValidate()
{
ValidateMeshTransform();
}
private float rotationVelocity = 0f;
private void UpdateMeshRotationToCamera()
{
// 각속도만 일부 적용, 실제 움직임에 가깝게 하면 이미지와 괴리 생김.
float deltaRotation = Mathf.DeltaAngle(lastRotationY, transform.eulerAngles.y);
currentAngularVelocity = deltaRotation / Time.fixedDeltaTime;
float desiredYaw = currentAngularVelocity / angularVelocityToYaw;
// get smoothed yaw
desiredWorldYaw = Mathf.SmoothDampAngle(desiredWorldYaw, desiredYaw, ref rotationVelocity, yawRotationSmoothTime);
// desiredWorldYaw = desiredYaw;
}
private void UpdateMeshRotationTilt()
{
if (visualGameObject is null) return;
// 현재 Y축 회전값과 각속도 계산
float deltaRotation = Mathf.DeltaAngle(lastRotationY, transform.eulerAngles.y);
currentAngularVelocity = deltaRotation / Time.fixedDeltaTime;
// 목표 틸트 각도 계산
float targetTilt = -currentAngularVelocity * angularVelocityMultiplier;
targetTilt = Mathf.Clamp(targetTilt, -maxRotationTiltAngle, maxRotationTiltAngle);
// 틸트 적용 또는 복귀
if (Mathf.Abs(currentAngularVelocity) > 0.1f)
{
currentRotationTilt =
Mathf.Lerp(currentRotationTilt, targetTilt, rotationTiltSpeed * Time.fixedDeltaTime);
}
else
{
// 입력이 없을 때는 원래 자세로 천천히 복귀
currentRotationTilt = Mathf.Lerp(currentRotationTilt, 0f, rotationTiltReturnSpeed * Time.fixedDeltaTime);
}
lastRotationY = transform.eulerAngles.y;
}
private void UpdateAccelerationTilt()
{
// 가속도 계산
float acceleration = (GetCurrentSpeed() - prevSpeed) / Time.fixedDeltaTime;
// 스프링 물리 시스템 구현
float springForce = -springStiffness * currentAccelTilt; // 복원력
float dampingForce = -springDamping * accelTiltVelocity; // 감쇠력
float accelerationForce = -acceleration * accelTiltForce; // 가속에 의한 힘
// 전체 힘 계산
float totalForce = springForce + dampingForce + accelerationForce;
// 가속도 계산 (F = ma, 질량은 1로 가정)
float tiltAcceleration = totalForce;
// 속도 업데이트
accelTiltVelocity += tiltAcceleration;
accelTiltVelocity *= accelTiltDamping; // 감쇠 적용
accelTiltVelocity *= Time.fixedDeltaTime;
// 위치(각도) 업데이트
currentAccelTilt = Mathf.Lerp(currentAccelTilt, currentAccelTilt + accelTiltVelocity,
accelTiltSpeed * Time.fixedDeltaTime);
currentAccelTilt = Mathf.Clamp(currentAccelTilt, -maxAccelTiltAngle, maxAccelTiltAngle);
prevSpeed = GetCurrentSpeed();
}
private void ApplyMeshTilt()
{
if (visualGameObject is null) return;
// 회전 틸트와 가속 틸트를 조합
// 메시에 최종 틸트 적용
visualGameObject.transform.localRotation = originalMeshRotation * Quaternion.Euler(
currentAccelTilt, // X축 (가속 틸트)
0, // Y축
currentRotationTilt // Z축 (회전 틸트)
);
if (overrideMeshYaw)
{
Vector3 position;
Quaternion rotation;
visualGameObject.transform.GetPositionAndRotation(out position, out rotation);
var desiredRotation = Quaternion.Euler(rotation.eulerAngles.x, desiredWorldYaw, rotation.eulerAngles.z);
visualGameObject.transform.SetPositionAndRotation(position, desiredRotation);
}
}
private void UpdateWaveMotion()
{
if (visualGameObject is null) return;
// 현재 속도에 비례하여 파도 주기 조절
float waveSpeedFactor = 1f + (GetCurrentSpeed() / waveUnitSpeed) * speedWaveMultiplier;
waveTime += Time.fixedDeltaTime * baseWaveFrequency * waveSpeedFactor;
float currentSpeedByUnit = GetCurrentSpeed() / waveUnitSpeed;
currentSpeedByUnit = Mathf.Clamp01(currentSpeedByUnit);
float waveHeight = Mathf.Lerp(minSpeedWaveHeight, maxSpeedWaveHeight, currentSpeedByUnit);
currentWaveHeight = waveHeight * Mathf.Sin(waveTime + waveRandomOffset);
}
private void ApplyMeshOffset()
{
if (visualGameObject is null) return;
Vector3 position = originalMeshPosition + (Vector3.up * currentWaveHeight);
visualGameObject.transform.localPosition = position;
}
private float GetCurrentSpeed()
{
return movement.CurrentSpeed;
}
}