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(); 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; } }