512 lines
20 KiB
C#
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
|
|
}
|
|
} |