using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.Serialization; public class VoyagePlayerShipMovement : MonoBehaviour { [Header("Movement Settings")] [SerializeField] private float maxSpeed = 20f; [SerializeField] private float rotationSpeed = 180f; [SerializeField] private float accelerationRate = 1f; [SerializeField] private float minSpeedThreshold = 0.1f; [SerializeField] private float dragFactor = 0.98f; private Vector3 currentVelocity; private Vector2 currentInput; private float targetSpeed; private float currentSpeed; [Header("Turn Settings")] [SerializeField] private float turnSpeedPenalty = 0.5f; // 선회 시 감속 정도 (0: 감속 없음, 1: 완전 정지) [SerializeField] private float maxTurnAngle = 180f; // 최대 감속이 적용되는 각도 #if UNITY_EDITOR [Header("Debug Settings")] [SerializeField] private bool showDebugLines = true; [SerializeField] private float debugLineLength = 4f; [SerializeField] private float debugLineHeightStep = 0.1f; private LineRenderer velocityLine; private LineRenderer forwardDirectionLine; private LineRenderer upDirectionLine; private LineRenderer inputDirectionLine; private bool lineRendererCreated = false; #endif // Rotation Tilt [Header("Rotation Tilt Settings")] [SerializeField] private float maxRotationTiltAngle = 15f; [SerializeField] private float rotationTiltSpeed = 5f; [SerializeField] private float RotationTiltReturnSpeed = 3f; // 원래 자세로 돌아오는 속도 [SerializeField] private float angularVelocityMultiplier = 2f; // 각속도 영향력 private float _currentRotationTilt = 0f; private float _lastRotationY; // 이전 프레임의 Y축 회전값 private float _currentAngularVelocity; // 현재 각속도 // Acceleration Tilt [Header("Acceleration Tilt Settings")] [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; // 스프링 감쇠 private float _currentAccelTilt; private float _accelTiltVelocity; private float _prevSpeed; // Wave offset [Header("Wave Settings")] [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; // 기준 속력 private float _waveTime; private float _waveRandomOffset; private float currentWaveHeight; [Header("Mesh Settings")] [SerializeField] private string meshObjectName = "Ship_Mesh"; private Transform _meshTransform; private Quaternion _originalMeshRotation; private Vector3 _originalMeshPosition; private void Start() { _meshTransform = transform.Find(meshObjectName); if (_meshTransform == null) { Debug.LogError($"메시 오브젝트를 찾을 수 없습니다: {meshObjectName}"); enabled = false; return; } _originalMeshPosition = _meshTransform.localPosition; _originalMeshRotation = _meshTransform.localRotation; _lastRotationY = transform.eulerAngles.y; _waveTime = 0f; _waveRandomOffset = Random.Range(-randomWaveOffset, randomWaveOffset); } private void FixedUpdate() { if (currentInput.magnitude > minSpeedThreshold) { HandleMovement(); HandleRotation(); } else { // 입력이 없을 때는 서서히 감속 currentSpeed = Mathf.Lerp(currentSpeed, 0f, accelerationRate * Time.fixedDeltaTime); } ApplyDrag(); ApplyMovement(); // Cosmetic mesh tilting UpdateMeshRotationTilt(); UpdateAccelerationTilt(); ApplyMeshTilt(); // Cosmetic mesh wave offset UpdateWaveMotion(); ApplyMeshOffset(); #if UNITY_EDITOR if (showDebugLines) { UpdateAllDebugLines(); } #endif } private void HandleMovement() { // 기본 목표 속도 계산 (입력 크기에 비례) float baseTargetSpeed = Mathf.Clamp01(currentInput.magnitude) * maxSpeed; // 현재 방향과 목표 방향 사이의 각도 계산 Vector3 inputDirection = new Vector3(currentInput.x, 0, currentInput.y).normalized; float angleDifference = Vector3.Angle(transform.forward, inputDirection); // 각도에 따른 속도 페널티 계산 (각도가 클수록 더 큰 감속) float turnPenaltyFactor = 1f - (angleDifference / maxTurnAngle * turnSpeedPenalty); turnPenaltyFactor = Mathf.Clamp01(turnPenaltyFactor); // 최종 목표 속도 계산 (기본 속도에 선회 페널티 적용) targetSpeed = baseTargetSpeed * turnPenaltyFactor; // 현재 속도를 목표 속도로 부드럽게 보간 currentSpeed = Mathf.Lerp(currentSpeed, targetSpeed, accelerationRate * Time.fixedDeltaTime); // 최소 임계값 이하면 완전히 정지 if (currentSpeed < minSpeedThreshold && targetSpeed < minSpeedThreshold) { currentSpeed = 0f; } // 현재 바라보는 방향으로 속도 벡터 업데이트 currentVelocity = transform.forward * currentSpeed; } private void HandleRotation() { if (currentInput.magnitude > minSpeedThreshold) { Vector3 inputDirection = new Vector3(currentInput.x, 0, currentInput.y).normalized; Quaternion targetRotation = Quaternion.LookRotation(inputDirection, Vector3.up); // 회전 속도를 현재 속도에 비례하도록 설정 float currentRotationSpeed = rotationSpeed * (currentSpeed / maxSpeed); currentRotationSpeed = Mathf.Max(currentRotationSpeed, rotationSpeed * 0.3f); // 기본 회전 적용 (오브젝트 전체) 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; } public void OnMove(InputAction.CallbackContext context) { currentInput = context.ReadValue(); } #if UNITY_EDITOR private void UpdateAllDebugLines() { if (lineRendererCreated == false) { lineRendererCreated = true; forwardDirectionLine = CreateLineRenderer("CurrentDirectionLine", Color.green); upDirectionLine = CreateLineRenderer("UpDirectionLine", Color.yellow); inputDirectionLine = CreateLineRenderer("InputDirectionLine", Color.red); velocityLine = CreateLineRenderer("VelocityLine", Color.blue); } // 전방 방향 표시 (기본 높이) DrawDebugLine(forwardDirectionLine, transform.forward, debugLineLength, 0); // 메시의 위쪽 방향 표시 (틸팅 반영) if (_meshTransform is not null) { DrawDebugLine(upDirectionLine, _meshTransform.up, debugLineLength, debugLineHeightStep); } // 입력 방향 표시 (두 단계 위) Vector3 inputDirection = new Vector3(currentInput.x, 0, currentInput.y).normalized; DrawDebugLine(inputDirectionLine, inputDirection, debugLineLength * currentInput.magnitude, debugLineHeightStep * 2); // 속도 벡터 표시 (세 단계 위) DrawDebugLine(velocityLine, currentVelocity.normalized, currentVelocity.magnitude, debugLineHeightStep * 3); } private LineRenderer CreateLineRenderer(string name, Color color) { GameObject lineObj = new GameObject(name); lineObj.transform.SetParent(transform); LineRenderer line = lineObj.AddComponent(); line.startWidth = 0.1f; line.endWidth = 0.1f; 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 DrawDebugLine(LineRenderer renderer, Vector3 direction, float length, float heightOffset) { if (!renderer) return; Vector3 position = transform.position + Vector3.up * heightOffset; renderer.SetPosition(0, position); renderer.SetPosition(1, position + direction * length); } #endif private void OnValidate() { // 에디터에서 메시 오브젝트 이름이 변경될 때 자동으로 찾기 if (Application.isEditor && !Application.isPlaying) { _meshTransform = transform.Find(meshObjectName); } } }