359 lines
14 KiB
C#
359 lines
14 KiB
C#
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<Vector2>();
|
|
}
|
|
|
|
#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<LineRenderer>();
|
|
|
|
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);
|
|
}
|
|
}
|
|
} |