using UnityEngine; using UnityEngine.InputSystem; /// /// 플레이어 배의 움직임을 제어하는 컴포넌트 /// 속도, 회전, 틸트, 파도 효과 등을 관리합니다. /// public class VoyagePlayerShipMovement : MonoBehaviour { #region Inspector Fields [Header("기본 이동 설정")] [Tooltip("배의 최대 이동 속도")] [SerializeField] private float maxSpeed = 20f; [SerializeField] private float accelerationRate = 1f; [SerializeField] private float dragFactor = 0.98f; [SerializeField] private float minSpeedThreshold = 0.1f; [Header("회전 설정")] [SerializeField] private float rotationSpeed = 270f; [SerializeField] private float minRotationSpeed = 90f; [SerializeField] private float rotationAccelerationRate = 5f; [SerializeField] private float turnSpeedPenalty = 0.5f; [SerializeField] private float maxTurnAngle = 180f; [Header("회전 틸트 설정")] [SerializeField] private float maxRotationTiltAngle = 15f; [SerializeField] private float rotationTiltSpeed = 5f; [SerializeField] private float rotationTiltReturnSpeed = 3f; [SerializeField] private float angularVelocityMultiplier = 2f; [Header("가속 틸트 설정")] [SerializeField] private float maxAccelTiltAngle = 15f; [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.2f; [SerializeField] private float maxSpeedWaveHeight = 0.05f; [SerializeField] private float baseWaveFrequency = 1f; [SerializeField] private float speedWaveMultiplier = 5f; [SerializeField] private float randomWaveOffset = 0.5f; [SerializeField] private float waveUnitSpeed = 10f; [Header("메시 설정")] [SerializeField] private Transform meshTransform; #endregion #region Private Fields private Vector3 currentVelocity; private Vector2 currentInput; private float currentRotationSpeed; private float targetSpeed; private float currentSpeed; // 회전 틸트 관련 private float currentRotationTilt; private float lastRotationY; private float currentAngularVelocity; // 가속 틸트 관련 private float currentAccelTilt; private float accelTiltVelocity; private float prevSpeed; // 파도 효과 관련 private float waveTime; private float waveRandomOffset; private float currentWaveHeight; // 메시 원본 상태 private Quaternion originalMeshRotation; private Vector3 originalMeshPosition; #endregion #region Unity Messages private void Start() { InitializeMeshTransform(); InitializeWaveEffect(); #if UNITY_EDITOR InitializeDebugVisuals(); #endif } private void FixedUpdate() { UpdateMovement(); UpdateVisualEffects(); #if UNITY_EDITOR UpdateDebugVisuals(); #endif } private void OnValidate() { ValidateMeshTransform(); } #endregion #region Movement Methods private void UpdateMovement() { if (IsMoving()) { HandleMovement(); HandleRotation(); } else { DecelerateMovement(); } ApplyDrag(); ApplyMovement(); } private bool IsMoving() { return currentInput.magnitude > minSpeedThreshold; } private void HandleMovement() { float baseTargetSpeed = CalculateBaseTargetSpeed(); float turnPenaltyFactor = CalculateTurnPenaltyFactor(); targetSpeed = baseTargetSpeed * turnPenaltyFactor; currentSpeed = Mathf.Lerp(currentSpeed, targetSpeed, accelerationRate * Time.fixedDeltaTime); if (ShouldStop()) { currentSpeed = 0f; } UpdateVelocityVector(); } private float CalculateBaseTargetSpeed() { return Mathf.Clamp01(currentInput.magnitude) * maxSpeed; } private float CalculateTurnPenaltyFactor() { Vector3 inputDirection = new Vector3(currentInput.x, 0, currentInput.y).normalized; float angleDifference = Vector3.Angle(transform.forward, inputDirection); return Mathf.Clamp01(1f - (angleDifference / maxTurnAngle * turnSpeedPenalty)); } private bool ShouldStop() { return currentSpeed < minSpeedThreshold && targetSpeed < minSpeedThreshold; } private void UpdateVelocityVector() { currentVelocity = transform.forward * currentSpeed; } #endregion #region Visual Effects private void UpdateVisualEffects() { if (meshTransform is null) return; UpdateMeshRotationTilt(); UpdateAccelerationTilt(); ApplyMeshTilt(); UpdateWaveMotion(); ApplyMeshOffset(); } private void HandleRotation() { if (IsMoving()) { Vector3 inputDirection = new Vector3(currentInput.x, 0, currentInput.y).normalized; Quaternion targetRotation = Quaternion.LookRotation(inputDirection, Vector3.up); // 회전 속도를 현재 속도에 비례하도록 설정 float desiredRotationSpeed = rotationSpeed * (currentSpeed / maxSpeed); desiredRotationSpeed = Mathf.Max(desiredRotationSpeed, minRotationSpeed); currentRotationSpeed = Mathf.Lerp(currentRotationSpeed, desiredRotationSpeed, rotationAccelerationRate * Time.fixedDeltaTime); // 기본 회전 적용 (오브젝트 전체) transform.rotation = Quaternion.RotateTowards( transform.rotation, targetRotation, currentRotationSpeed * Time.fixedDeltaTime ); } } private void UpdateMeshRotationTilt() { if (meshTransform is null) return; // 현재 Y축 회전값과 각속도 계산 float currentRotationY = transform.eulerAngles.y; float deltaRotation = Mathf.DeltaAngle(lastRotationY, currentRotationY); 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 = currentRotationY; } private void UpdateAccelerationTilt() { // 가속도 계산 float acceleration = (currentSpeed - 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 = currentSpeed; } private void ApplyMeshTilt() { if (meshTransform is null) return; // 회전 틸트와 가속 틸트를 조합 // 메시에 최종 틸트 적용 meshTransform.localRotation = originalMeshRotation * Quaternion.Euler( currentAccelTilt, // X축 (가속 틸트) 0, // Y축 currentRotationTilt // Z축 (회전 틸트) ); } private void UpdateWaveMotion() { if (meshTransform is null) return; // 현재 속도에 비례하여 파도 주기 조절 float waveSpeedFactor = 1f + (currentSpeed / waveUnitSpeed) * speedWaveMultiplier; waveTime += Time.fixedDeltaTime * baseWaveFrequency * waveSpeedFactor; float currentSpeedByUnit = currentSpeed / waveUnitSpeed; currentSpeedByUnit = Mathf.Clamp01(currentSpeedByUnit); float waveHeight = Mathf.Lerp(minSpeedWaveHeight, maxSpeedWaveHeight, currentSpeedByUnit); currentWaveHeight = waveHeight * Mathf.Sin(waveTime + waveRandomOffset); } private void ApplyMeshOffset() { if (meshTransform is null) return; Vector3 position = originalMeshPosition + (Vector3.up * currentWaveHeight); meshTransform.localPosition = position; } private void ApplyDrag() { currentSpeed *= dragFactor; // 최소 속도 이하면 완전히 정지 if (currentSpeed < minSpeedThreshold) { currentSpeed = 0f; } // 현재 방향으로 감속된 속도 적용 currentVelocity = transform.forward * currentSpeed; } private void ApplyMovement() { transform.position += currentVelocity * Time.fixedDeltaTime; } private void DecelerateMovement() { // 입력이 없을 때는 서서히 감속 currentSpeed = Mathf.Lerp(currentSpeed, 0f, accelerationRate * Time.fixedDeltaTime); currentRotationSpeed = 0; } #endregion #region Input Handling public void OnMove(InputAction.CallbackContext context) { currentInput = context.ReadValue(); } #endregion #region Initialization private void InitializeMeshTransform() { if (meshTransform is null) { Debug.LogError("Mesh Transform이 할당되지 않았습니다."); enabled = false; return; } originalMeshPosition = meshTransform.localPosition; originalMeshRotation = meshTransform.localRotation; lastRotationY = transform.eulerAngles.y; } private void InitializeWaveEffect() { waveTime = 0f; waveRandomOffset = Random.Range(-randomWaveOffset, randomWaveOffset); } private void ValidateMeshTransform() { if (Application.isEditor && !Application.isPlaying && meshTransform is null) { Debug.LogWarning("Mesh Transform을 Inspector에서 할당해주세요."); } } #endregion #if UNITY_EDITOR [Header("Debug Visualization")] [SerializeField] private bool showDebugVisuals = true; [SerializeField] private float debugLineLength = 5f; [SerializeField] private float debugLineWidth = 0.1f; private LineRenderer _speedLineRenderer; private LineRenderer _rotationSpeedLineRenderer; private LineRenderer _rotationDeltaLineRenderer; private LineRenderer _TiltLineRenderer; private LineRenderer _waveHeightLineRenderer; private LineRenderer _wavePatternLineRenderer; private void InitializeDebugVisuals() { if (!showDebugVisuals) return; // 속도 표시 _speedLineRenderer = CreateLineRenderer("SpeedLine", Color.green); // 회전 방향 표시 _rotationSpeedLineRenderer = CreateLineRenderer("RotationSpeedLine", Color.magenta); // 회전 방향 표시 _rotationDeltaLineRenderer = CreateLineRenderer("RotationDeltaLine", Color.yellow); // 틸트 표시 _TiltLineRenderer = CreateLineRenderer("TiltLine", Color.red); // 파도 높이 표시 _waveHeightLineRenderer = CreateLineRenderer("WaveHeightLine", Color.blue); // 파도 패턴 표시 _wavePatternLineRenderer = CreateLineRenderer("WavePatternLine", Color.cyan); _wavePatternLineRenderer.positionCount = 50; // 파도 패턴을 위한 더 많은 점 } private void UpdateDebugVisuals() { if (!showDebugVisuals) return; // 속도 벡터 표시 UpdateSpeedLine(); // 회전 방향 및 각속도 표시 UpdateRotationSpeedLine(); UpdateRotationDeltaLine(); // 회전 틸트 표시 UpdateTiltLine(); // 파도 높이와 패턴 표시 UpdateWaveVisualization(); } private void UpdateSpeedLine() { Vector3 start = transform.position + Vector3.up * 1.5f; Vector3 end = start + transform.forward * (currentSpeed / maxSpeed) * debugLineLength * 2; DrawLine(_speedLineRenderer, start, end); } private void UpdateRotationSpeedLine() { Vector3 start = transform.position + Vector3.up * 1.2f; // 각속도를 호로 표현 if (Mathf.Abs(currentRotationSpeed) > 0.1f) { Vector3[] arcPoints = new Vector3[10]; float radius = debugLineLength * 1f; float angleStep = currentRotationSpeed * 1f / (arcPoints.Length - 1); for (int i = 0; i < arcPoints.Length; i++) { float angle = angleStep * i; Vector3 point = start + Quaternion.Euler(0, angle, 0) * transform.forward * radius; arcPoints[i] = point; } _rotationSpeedLineRenderer.positionCount = arcPoints.Length; _rotationSpeedLineRenderer.SetPositions(arcPoints); } else { _rotationSpeedLineRenderer.positionCount = 0; } } private void UpdateRotationDeltaLine() { float deltaAngle = 0f; if (currentInput.magnitude > minSpeedThreshold) { Vector3 inputDirection = new Vector3(currentInput.x, 0, currentInput.y).normalized; Quaternion targetRotation = Quaternion.LookRotation(inputDirection, Vector3.up); deltaAngle = Quaternion.Angle(transform.rotation, targetRotation); } Vector3 start = transform.position + Vector3.up * 1.2f; // 각속도를 호로 표현 if (Mathf.Abs(deltaAngle) > 0.1f) { Vector3[] arcPoints = new Vector3[10]; float radius = debugLineLength * 1.05f; float angleStep = deltaAngle * 1f / (arcPoints.Length - 1); for (int i = 0; i < arcPoints.Length; i++) { float angle = angleStep * i; Vector3 point = start + Quaternion.Euler(0, angle, 0) * transform.forward * radius; arcPoints[i] = point; } _rotationDeltaLineRenderer.positionCount = arcPoints.Length; _rotationDeltaLineRenderer.SetPositions(arcPoints); } else { _rotationDeltaLineRenderer.positionCount = 0; } } private void UpdateTiltLine() { Vector3 start = transform.position + Vector3.up * 1.5f; Vector3 tiltDirection = meshTransform.up; DrawLine(_TiltLineRenderer, start, start + tiltDirection * debugLineLength * 0.4f); } private void UpdateWaveVisualization() { // 현재 파도 높이 표시 Vector3 waveStart = transform.position + Vector3.up * 1.5f - transform.forward * 1.5f; Vector3 waveEnd = waveStart + Vector3.up * currentWaveHeight * debugLineLength; DrawLine(_waveHeightLineRenderer, waveStart, waveEnd); // 파도 패턴 시각화 Vector3[] wavePoints = new Vector3[_wavePatternLineRenderer.positionCount]; float waveLength = debugLineLength * 2f; for (int i = 0; i < wavePoints.Length; i++) { float t = (float)i / (_wavePatternLineRenderer.positionCount - 1); float x = t * waveLength - waveLength * 0.5f; float currentSpeedByUnit = currentSpeed / waveUnitSpeed; currentSpeedByUnit = Mathf.Clamp01(currentSpeedByUnit); float waveHeight = Mathf.Lerp(minSpeedWaveHeight, maxSpeedWaveHeight, currentSpeedByUnit); float y = Mathf.Sin((waveTime + x) * baseWaveFrequency) * waveHeight; wavePoints[i] = transform.position + Vector3.right * x + Vector3.up * (y + 2f); // 높이 오프셋 wavePoints[i] += Vector3.back * 3f + Vector3.down * 1f; } _wavePatternLineRenderer.SetPositions(wavePoints); } private LineRenderer CreateLineRenderer(string name, Color color) { GameObject lineObj = new GameObject(name); lineObj.transform.SetParent(transform); LineRenderer line = lineObj.AddComponent(); line.startWidth = debugLineWidth; line.endWidth = debugLineWidth; line.material = new Material(Shader.Find("Universal Render Pipeline/Unlit")); line.startColor = color; line.endColor = color; line.positionCount = 2; line.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; line.receiveShadows = false; line.material.color = color; return line; } private void DrawLine(LineRenderer line, Vector3 start, Vector3 end) { if (line is null) return; line.positionCount = 2; line.SetPosition(0, start); line.SetPosition(1, end); } #endif }