490 lines
18 KiB
C#
490 lines
18 KiB
C#
using System;
|
|
using System.Collections;
|
|
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 = -1;
|
|
|
|
[Range(0.01f, 0.5f), Tooltip("전방 감지 거리")]
|
|
public float forwardCheckDistance = 0.1f;
|
|
|
|
[Range(0.1f, 10.0f), Tooltip("지면 감지 거리")]
|
|
public float groundCheckDistance = 2.0f;
|
|
|
|
[Range(0.0f, 0.5f), Tooltip("지면 인식 허용 거리")]
|
|
public float groundCheckThreshold = 0.01f;
|
|
}
|
|
|
|
[Serializable]
|
|
public class MovementOption
|
|
{
|
|
[Range(1f, 10f), Tooltip("이동 속도")]
|
|
public float moveSpeed = 10f;
|
|
|
|
[Range(1f, 75f), Tooltip("등반 가능한 경사각")]
|
|
public float maxSlopeAngle = 50f;
|
|
|
|
[Range(1f, 50f), Tooltip("대쉬 속도")]
|
|
public float dashSpeed = 30f;
|
|
|
|
[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 float capsuleRadiusDifferent;
|
|
private float castRadius;
|
|
|
|
private Vector3 CapsuleTopCenterPoint => new(transform.position.x,
|
|
transform.position.y + MyComponents.capsuleCollider.height - MyComponents.capsuleCollider.radius,
|
|
transform.position.z);
|
|
|
|
private Vector3 CapsuleBottomCenterPoint => new(transform.position.x,
|
|
transform.position.y + MyComponents.capsuleCollider.radius,
|
|
transform.position.z);
|
|
|
|
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;
|
|
}
|
|
|
|
var capsuleColliderRadius = MyComponents.capsuleCollider.radius;
|
|
castRadius = capsuleColliderRadius * 0.9f;
|
|
capsuleRadiusDifferent = capsuleColliderRadius - castRadius + 0.05f;
|
|
}
|
|
|
|
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;
|
|
|
|
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.SphereCast(CapsuleBottomCenterPoint, castRadius,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;
|
|
|
|
// 경사각 이중검증 (수직 레이캐스트) : 뾰족하거나 각진 부분 체크
|
|
//if (State.isOnSteepSlope)
|
|
//{
|
|
// Vector3 ro = hit.point + Vector3.up * 0.1f;
|
|
// Vector3 rd = Vector3.down;
|
|
// bool rayD =
|
|
// Physics.SphereCast(ro, 0.09f, rd, out var hitRayD, 0.2f, COption.groundLayerMask, QueryTriggerInteraction.Ignore);
|
|
|
|
// Current.groundVerticalSlopeAngle = rayD ? Vector3.Angle(hitRayD.normal, Vector3.up) : Current.groundSlopeAngle;
|
|
|
|
// State.isOnSteepSlope = Current.groundVerticalSlopeAngle >= MOption.maxSlopeAngle;
|
|
//}
|
|
|
|
MyCurrentValue.groundDistance = Mathf.Max(hit.distance - capsuleRadiusDifferent - MyCheckOption.groundCheckThreshold, 0f);
|
|
MyCurrentState.isGrounded = (MyCurrentValue.groundDistance <= 0.0001f) && !MyCurrentState.isOnSteepSlope;
|
|
|
|
GizmosUpdateValue(ref gzGroundTouch, hit.point);
|
|
}
|
|
|
|
MyCurrentValue.groundCross = Vector3.Cross(MyCurrentValue.groundNormal, Vector3.up);
|
|
}
|
|
|
|
/// <summary> 전방 장애물 검사 : 레이어 관계 없이 trigger가 아닌 모든 장애물 검사 </summary>
|
|
private void CheckForward()
|
|
{
|
|
var obstacleRaycast = Physics.CapsuleCast(CapsuleBottomCenterPoint, CapsuleTopCenterPoint, castRadius,
|
|
MyCurrentValue.currentMoveDirection + Vector3.down * 0.1f,
|
|
out var hit, MyCheckOption.forwardCheckDistance, -1, QueryTriggerInteraction.Ignore);
|
|
|
|
MyCurrentState.isForwardBlocked = false;
|
|
if (obstacleRaycast)
|
|
{
|
|
var forwardObstacleAngle = Vector3.Angle(hit.normal, Vector3.up);
|
|
MyCurrentState.isForwardBlocked = forwardObstacleAngle >= MyMovementOption.maxSlopeAngle;
|
|
|
|
GizmosUpdateValue(ref gzForwardTouch, hit.point);
|
|
}
|
|
}
|
|
|
|
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);
|
|
Gizmos.DrawWireSphere(CapsuleTopCenterPoint, castRadius);
|
|
Gizmos.DrawWireSphere(CapsuleBottomCenterPoint, castRadius);
|
|
}
|
|
|
|
[System.Diagnostics.Conditional("UNITY_EDITOR")]
|
|
private void GizmosUpdateValue<T>(ref T variable, in T value)
|
|
{
|
|
variable = value;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
} |