OldBlueWater/BlueWater/Assets/02.Scripts/Character/CombatPlayer/PhysicsMovement.cs
NTG 7b4408247e #165 전투 플레이어 움직임 보완 작업
+ 전방 장애물 감지 보완
+ 경사면에서 움직임 보완
2024-02-18 04:40:14 +09:00

512 lines
20 KiB
C#

using System;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.InputSystem;
// ReSharper disable once CheckNamespace
namespace BlueWaterProject
{
public class PhysicsMovement : MonoBehaviour, IStun
{
/***********************************************************************
* Definitions
***********************************************************************/
#region Class
[Serializable]
public class Components
{
public CapsuleCollider capsuleCollider;
public Rigidbody rb;
[ShowIf("@false")]
public Animator animator;
public Transform[] spawnPosition;
}
[Serializable]
public class CheckOption
{
[Tooltip("지면으로 체크할 레이어 설정")]
public LayerMask groundLayer;
[Tooltip("장애물로 체크할 레이어 설정")]
public LayerMask obstacleLayer = -1;
[Range(0.01f, 0.5f), Tooltip("전방 감지 거리")]
public float forwardCheckDistance = 0.1f;
[Range(0.0f, 0.5f), Tooltip("전방 지면 인식 허용 거리")]
public float forwardCheckThreshold = 0.4f;
[Range(0.1f, 10.0f), Tooltip("지면 감지 거리")]
public float groundCheckDistance = 2.0f;
[Range(0.0f, 1f), Tooltip("지면 인식 허용 거리")]
public float groundCheckThreshold = 0.2f;
}
[Serializable]
public class MovementOption
{
[Range(1f, 10f), Tooltip("이동 속도")]
public float moveSpeed = 10f;
[Range(1f, 75f), Tooltip("등반 가능한 경사각")]
public float maxSlopeAngle = 30f;
[Range(1f, 50f), Tooltip("대쉬 속도")]
public float dashSpeed = 20f;
[Range(0.1f, 1f), Tooltip("대쉬 시간")]
public float dashTime = 0.2f;
[Range(0f, 5f), Tooltip("대쉬 쿨타임")]
public float dashCooldown = 0.5f;
}
[Serializable]
[DisableIf("@true")]
public class CurrentState
{
public bool enableMoving = true;
public bool isMoving;
public bool isGrounded;
public bool isOnSlope;
public bool isOnSteepSlope;
public bool isForwardBlocked;
public bool isOutOfControl;
public bool isDashing;
public bool enableDash = true;
public bool isStunned;
}
[Serializable]
[DisableIf("@true")]
public class CurrentValue
{
public Vector2 movementInput;
public Vector3 currentMoveDirection;
public Vector3 previousMoveDirection = Vector3.back;
public Vector3 groundNormal;
public Vector3 groundCross;
public Vector3 horizontalVelocity;
[Space]
public float outOfControlDuration;
[Space]
public float groundDistance;
public float groundSlopeAngle; // 현재 바닥의 경사각
public float forwardSlopeAngle; // 캐릭터가 바라보는 방향의 경사각
[Space]
public Vector3 gravity;
}
#endregion
/***********************************************************************
* Variables
***********************************************************************/
#region Variables
[field: SerializeField] public Components MyComponents { get; private set; } = new();
[field: SerializeField] public CheckOption MyCheckOption { get; private set; } = new();
[field: SerializeField] public MovementOption MyMovementOption { get; private set; } = new();
[field: SerializeField] public CurrentState MyCurrentState { get; set; } = new();
[field: SerializeField] public CurrentValue MyCurrentValue { get; set; } = new();
[Title("효과")]
[SerializeField] private ParticleSystem stunParticle;
public bool IsStunned
{
get => MyCurrentState.isStunned;
set => MyCurrentState.isStunned = value;
}
private Vector3 CapsuleTop => MyComponents.rb.position + (MyComponents.capsuleCollider.center +
Vector3.up * (MyComponents.capsuleCollider.height * 0.5f - MyComponents.capsuleCollider.radius))
* transform.localScale.x;
private Vector3 CapsuleBottom => MyComponents.rb.position + (MyComponents.capsuleCollider.center -
Vector3.up * (MyComponents.capsuleCollider.height * 0.5f - MyComponents.capsuleCollider.radius))
* transform.localScale.x;
private float CapsuleRadius => MyComponents.capsuleCollider.radius * transform.localScale.x * 0.9f;
private float CapsuleHeight => MyComponents.capsuleCollider.height * transform.localScale.y;
public static readonly int IsDashingHash = Animator.StringToHash("isDashing");
#endregion
/***********************************************************************
* Unity Events
***********************************************************************/
#region Unity Events
private void Start()
{
InitRigidbody();
InitCapsuleCollider();
InitStartValue();
}
private void FixedUpdate()
{
if (!MyCurrentState.enableMoving || IsStunned) return;
InputMove();
CheckGround();
CheckForward();
UpdateValues();
CalculateMovements();
ApplyMovementsToRigidbody();
}
#endregion
/***********************************************************************
* Init Methods
***********************************************************************/
#region Init Methods
private void InitRigidbody()
{
if (TryGetComponent(out MyComponents.rb)) return;
MyComponents.rb = gameObject.AddComponent<Rigidbody>();
MyComponents.rb.constraints = RigidbodyConstraints.FreezeRotation;
MyComponents.rb.interpolation = RigidbodyInterpolation.Interpolate;
MyComponents.rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
MyComponents.rb.useGravity = true;
}
private void InitCapsuleCollider()
{
if (!TryGetComponent(out MyComponents.capsuleCollider))
{
MyComponents.capsuleCollider = gameObject.AddComponent<CapsuleCollider>();
MyComponents.capsuleCollider.height = 2f;
MyComponents.capsuleCollider.center = Vector3.up;
MyComponents.capsuleCollider.radius = 0.5f;
}
}
private void InitStartValue()
{
MyCurrentValue.gravity = Physics.gravity;
if (stunParticle)
{
stunParticle.Stop();
}
}
#endregion
/***********************************************************************
* Interfaces
***********************************************************************/
#region Interfaces
public void Stun(float stunTime)
{
if (MyCurrentState.isDashing) return;
IsStunned = true;
MyComponents.rb.velocity = Vector3.zero;
MyCurrentState.isMoving = false;
if (stunParticle)
{
stunParticle.Play();
}
StartCoroutine(Utils.CoolDown(stunTime, StopStun));
}
private void StopStun()
{
if (stunParticle)
{
stunParticle.Stop();
}
IsStunned = false;
}
#endregion
/***********************************************************************
* PlayerInput
***********************************************************************/
#region PlayerInput
private void OnMove(InputValue value)
{
MyCurrentValue.movementInput = value.Get<Vector2>();
}
private void OnDash()
{
if (!MyCurrentState.enableDash || MyCurrentState.isDashing) return;
MyComponents.animator.SetBool(IsDashingHash, true);
}
#endregion
/***********************************************************************
* Methods
***********************************************************************/
#region Methods
private void InputMove()
{
if (MyCurrentValue.currentMoveDirection != Vector3.zero)
{
MyCurrentValue.previousMoveDirection = MyCurrentValue.currentMoveDirection;
}
MyCurrentValue.currentMoveDirection = MyCurrentState.isDashing
? MyCurrentValue.previousMoveDirection
: new Vector3(MyCurrentValue.movementInput.x, 0,MyCurrentValue.movementInput.y).normalized;
MyCurrentState.isMoving = MyCurrentValue.currentMoveDirection != Vector3.zero;
}
/// <summary> 하단 지면 검사 </summary>
private void CheckGround()
{
MyCurrentValue.groundDistance = float.MaxValue;
MyCurrentValue.groundNormal = Vector3.up;
MyCurrentValue.groundSlopeAngle = 0f;
MyCurrentValue.forwardSlopeAngle = 0f;
var groundRaycast = Physics.CapsuleCast(CapsuleBottom, CapsuleTop, CapsuleRadius,
Vector3.down, out var hit, MyCheckOption.groundCheckDistance, MyCheckOption.groundLayer, QueryTriggerInteraction.Ignore);
MyCurrentState.isGrounded = false;
if (groundRaycast)
{
MyCurrentValue.groundNormal = hit.normal;
MyCurrentValue.groundSlopeAngle = Vector3.Angle(MyCurrentValue.groundNormal, Vector3.up);
MyCurrentValue.forwardSlopeAngle = Vector3.Angle(MyCurrentValue.groundNormal, MyCurrentValue.currentMoveDirection) - 90f;
MyCurrentState.isOnSlope = MyCurrentValue.groundSlopeAngle > 0f && MyCurrentValue.groundSlopeAngle < MyMovementOption.maxSlopeAngle;
MyCurrentState.isOnSteepSlope = MyCurrentValue.groundSlopeAngle >= MyMovementOption.maxSlopeAngle;
MyCurrentValue.groundDistance = Mathf.Max(hit.distance, 0f);
MyCurrentState.isGrounded = MyCurrentValue.groundDistance <= MyCheckOption.groundCheckThreshold;
GizmosUpdateValue(ref gzGroundTouch, hit.point);
}
MyCurrentValue.groundCross = Vector3.Cross(MyCurrentValue.groundNormal, Vector3.up);
}
/// <summary> 전방 장애물 검사 : 레이어 관계 없이 trigger가 아닌 모든 장애물 검사 </summary>
private void CheckForward()
{
var start = MyComponents.rb.position + Vector3.up * (CapsuleHeight - (CapsuleHeight - MyCheckOption.forwardCheckThreshold) * 0.5f);
var boxScale = new Vector3(CapsuleRadius, (CapsuleHeight - MyCheckOption.forwardCheckThreshold) * 0.5f, CapsuleRadius);
var raycast = Physics.BoxCast(start, boxScale, MyCurrentValue.currentMoveDirection, out var hit,
Quaternion.identity, MyCheckOption.forwardCheckDistance,MyCheckOption.obstacleLayer, QueryTriggerInteraction.Ignore);
MyCurrentState.isForwardBlocked = false;
if (raycast)
{
MyCurrentState.isForwardBlocked = true;
return;
}
if (MyCurrentState.isOnSteepSlope)
{
start = MyComponents.rb.position + Vector3.up * 0.9f + MyCurrentValue.currentMoveDirection * MyCheckOption.forwardCheckDistance;
boxScale = new Vector3(CapsuleRadius, (CapsuleHeight - 0.4f) * 0.5f, CapsuleRadius);
var raycast2 = Physics.BoxCast(start, boxScale, Vector3.down, out var hit2,
Quaternion.identity, MyCheckOption.groundCheckDistance,MyCheckOption.groundLayer, QueryTriggerInteraction.Ignore);
if (raycast2)
{
var angle = Vector3.Angle(hit2.normal, Vector3.up);
if (angle < MyMovementOption.maxSlopeAngle)
{
MyCurrentState.isOnSteepSlope = false;
}
}
}
}
private void UpdateValues()
{
MyCurrentState.isOutOfControl = MyCurrentValue.outOfControlDuration > 0f;
if (MyCurrentState.isOutOfControl)
{
MyCurrentValue.outOfControlDuration -= Time.fixedDeltaTime;
MyCurrentValue.currentMoveDirection = Vector3.zero;
}
}
private void CalculateMovements()
{
if (MyCurrentState.isOutOfControl)
{
MyComponents.rb.useGravity = true;
MyCurrentValue.horizontalVelocity = Vector3.zero;
return;
}
var speed = 0f;
if (MyCurrentState.isDashing)
{
speed = MyMovementOption.dashSpeed;
}
else
{
speed = MyCurrentState.isMoving ? MyMovementOption.moveSpeed : 0f;
}
if (MyCurrentState.isOnSlope)
{
MyComponents.rb.useGravity = false;
if (MyCurrentState.isMoving)
{
var changeMoveDirection = Vector3.ProjectOnPlane(MyCurrentValue.currentMoveDirection, MyCurrentValue.groundNormal).normalized;
MyCurrentValue.horizontalVelocity = changeMoveDirection * speed;
}
else
{
MyCurrentValue.horizontalVelocity = Vector3.zero;
}
return;
}
MyComponents.rb.useGravity = true;
if (MyCurrentState.isForwardBlocked || !MyCurrentState.isGrounded)
{
MyCurrentValue.horizontalVelocity = Vector3.zero;
}
else
{
MyCurrentValue.horizontalVelocity = MyCurrentValue.currentMoveDirection * speed;
}
}
/// <summary> 리지드바디 최종 속도 적용 </summary>
private void ApplyMovementsToRigidbody()
{
Vector3 finalVelocity;
if (MyCurrentState.isOutOfControl || MyCurrentState.isOnSteepSlope || !MyCurrentState.isGrounded)
{
// var velocity = MyComponents.rb.velocity;
// finalVelocity = MyComponents.rb.position + new Vector3(velocity.x, MyCurrentValue.gravity.y, velocity.z) * Time.fixedDeltaTime;
// MyComponents.rb.MovePosition(finalVelocity);
return;
}
if (MyCurrentValue.horizontalVelocity == Vector3.zero)
{
MyComponents.rb.velocity = Vector3.zero;
return;
}
finalVelocity = MyComponents.rb.position + MyCurrentValue.horizontalVelocity * Time.fixedDeltaTime;
MyComponents.rb.MovePosition(finalVelocity);
// finalVelocity = MyCurrentValue.horizontalVelocity;
// MyComponents.rb.velocity = finalVelocity;
}
public void Move(Vector3 position) => MyComponents.rb.position = position;
public void MoveToCurrentDirection(float speed)
{
var finalVelocity = MyComponents.rb.position + MyCurrentValue.previousMoveDirection * (speed * Time.fixedDeltaTime);
MyComponents.rb.MovePosition(finalVelocity);
// var finalVelocity = MyCurrentValue.previousMoveDirection * speed;
// MyComponents.rb.velocity = finalVelocity;
}
public void SetEnableMoving(bool value) => MyCurrentState.enableMoving = value;
public bool GetIsMoving() => MyCurrentState.isMoving;
public bool GetIsDashing() => MyCurrentState.isDashing;
public float GetDashCooldown() => MyMovementOption.dashCooldown;
public float GetDashTime() => MyMovementOption.dashTime;
public void SetIsDashing(bool value) => MyCurrentState.isDashing = value;
public void SetEnableDashing(bool value) => MyCurrentState.enableDash = value;
public Vector3 GetPreviousMoveDirection() => MyCurrentValue.previousMoveDirection;
public void SetPreviousMoveDirection(Vector3 value) => MyCurrentValue.previousMoveDirection = value;
public void SetAnimator(Animator animator) => MyComponents.animator = animator;
public Vector3 GetCurrentPosition() => MyComponents.rb.position;
public void SetIsTrigger(bool value) => MyComponents.capsuleCollider.isTrigger = value;
public void SetUseGravity(bool value) => MyComponents.rb.useGravity = value;
#endregion
/***********************************************************************
* Gizmos, GUI
***********************************************************************/
#region Gizmos, GUI
private Vector3 gzGroundTouch;
private Vector3 gzForwardTouch;
[Header("Gizmos Option")] public bool showGizmos = true;
[SerializeField, Range(0.01f, 2f)] private float gizmoRadius = 0.05f;
[System.Diagnostics.Conditional("UNITY_EDITOR")]
private void OnDrawGizmos()
{
if (Application.isPlaying == false) return;
if (!showGizmos) return;
if (!enabled) return;
Gizmos.color = Color.red;
Gizmos.DrawSphere(gzGroundTouch, gizmoRadius);
if (MyCurrentState.isForwardBlocked)
{
Gizmos.color = Color.blue;
Gizmos.DrawSphere(gzForwardTouch, gizmoRadius);
}
Gizmos.color = Color.blue;
Gizmos.DrawLine(gzGroundTouch - MyCurrentValue.groundCross,
gzGroundTouch + MyCurrentValue.groundCross);
Gizmos.color = new Color(0.5f, 1.0f, 0.8f, 0.8f);
// CapsuleCast의 시작과 끝 위치를 표현
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(CapsuleBottom, CapsuleRadius);
Gizmos.DrawWireSphere(CapsuleTop, CapsuleRadius);
// CapsuleCast가 이동할 경로 표현
Gizmos.color = Color.blue;
Gizmos.DrawLine(CapsuleBottom, CapsuleBottom + Vector3.down * MyCheckOption.groundCheckDistance);
Gizmos.DrawLine(CapsuleTop, CapsuleTop + Vector3.down * MyCheckOption.groundCheckDistance);
Gizmos.color = Color.green;
var start = MyComponents.rb.position + Vector3.up * 0.9f +
MyCurrentValue.previousMoveDirection * MyCheckOption.forwardCheckDistance;
var boxScale = new Vector3(CapsuleRadius, (CapsuleHeight - 0.4f) * 0.5f, CapsuleRadius);
Gizmos.DrawWireCube(start, boxScale * 2);
// CapsuleCast의 종료 위치에 대한 캡슐을 표현
//Gizmos.DrawWireSphere(CapsuleBottom + Vector3.down * MyCheckOption.groundCheckDistance, CapsuleRadius);
//Gizmos.DrawWireSphere(CapsuleTop + Vector3.down * MyCheckOption.groundCheckDistance, CapsuleRadius);
}
[System.Diagnostics.Conditional("UNITY_EDITOR")]
private void GizmosUpdateValue<T>(ref T variable, in T value)
{
variable = value;
}
#endregion
}
}