using System; using Sirenix.OdinInspector; using UnityEngine; using UnityEngine.InputSystem; // ReSharper disable once CheckNamespace namespace BlueWaterProject { public class PhysicsMovement : MonoBehaviour { /*********************************************************************** * Definitions ***********************************************************************/ #region Class [Serializable] public class Components { public CapsuleCollider capsuleCollider; public Rigidbody rb; [ShowIf("@false")] public Animator animator; } [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; } [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(); 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) 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(); 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(); 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; } #endregion /*********************************************************************** * PlayerInput ***********************************************************************/ #region PlayerInput private void OnMove(InputValue value) { MyCurrentValue.movementInput = value.Get(); } 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; } /// 하단 지면 검사 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); } /// 전방 장애물 검사 : 레이어 관계 없이 trigger가 아닌 모든 장애물 검사 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; } } /// 리지드바디 최종 속도 적용 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); } public void Move(Vector3 velocity) { MyComponents.rb.position = velocity; } public void MoveToCurrentDirection(float speed) { var finalVelocity = MyComponents.rb.position + MyCurrentValue.previousMoveDirection * speed * Time.fixedDeltaTime; MyComponents.rb.MovePosition(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(ref T variable, in T value) { variable = value; } [SerializeField, Space] private bool showGUI = true; [SerializeField] private int guiTextSize = 28; private float prevForwardSlopeAngle; private void OnGUI() { if (Application.isPlaying == false) return; if (!showGUI) return; if (!enabled) return; GUIStyle labelStyle = GUI.skin.label; labelStyle.normal.textColor = Color.yellow; labelStyle.fontSize = Math.Max(guiTextSize, 20); prevForwardSlopeAngle = MyCurrentValue.forwardSlopeAngle == -90f ? prevForwardSlopeAngle : MyCurrentValue.forwardSlopeAngle; var oldColor = GUI.color; GUI.color = new Color(0f, 0f, 0f, 0.5f); GUI.Box(new Rect(40, 40, 420, 240), ""); GUI.color = oldColor; GUILayout.BeginArea(new Rect(50, 50, 1000, 500)); GUILayout.Label($"Ground Height : {Mathf.Min(MyCurrentValue.groundDistance, 99.99f): 00.00}", labelStyle); GUILayout.Label($"Slope Angle(Ground) : {MyCurrentValue.groundSlopeAngle: 00.00}", labelStyle); GUILayout.Label($"Slope Angle(Forward) : {prevForwardSlopeAngle: 00.00}", labelStyle); GUILayout.Label($"Allowed Slope Angle : {MyMovementOption.maxSlopeAngle: 00.00}", labelStyle); GUILayout.Label($"Current Speed Mag : {MyCurrentValue.horizontalVelocity.magnitude: 00.00}", labelStyle); GUILayout.EndArea(); float sWidth = Screen.width; float sHeight = Screen.height; GUIStyle RTLabelStyle = GUI.skin.label; RTLabelStyle.fontSize = 20; RTLabelStyle.normal.textColor = Color.green; oldColor = GUI.color; GUI.color = new Color(1f, 1f, 1f, 0.5f); GUI.Box(new Rect(sWidth - 355f, 5f, 340f, 100f), ""); GUI.color = oldColor; var yPos = 10f; GUI.Label(new Rect(sWidth - 350f, yPos, 150f, 30f), $"Speed : {MyMovementOption.moveSpeed: 00.00}", RTLabelStyle); MyMovementOption.moveSpeed = GUI.HorizontalSlider(new Rect(sWidth - 180f, yPos + 10f, 160f, 20f), MyMovementOption.moveSpeed, 1f, 10f); yPos += 20f; GUI.Label(new Rect(sWidth - 350f, yPos, 150f, 30f), $"Max Slope : {MyMovementOption.maxSlopeAngle: 00}", RTLabelStyle); MyMovementOption.maxSlopeAngle = (int)GUI.HorizontalSlider( new Rect(sWidth - 180f, yPos + 10f, 160f, 20f), MyMovementOption.maxSlopeAngle, 1f, 75f); yPos += 20f; GUI.Label(new Rect(sWidth - 350f, yPos, 180f, 30f), $"TimeScale : {Time.timeScale: 0.00}", RTLabelStyle); Time.timeScale = GUI.HorizontalSlider( new Rect(sWidth - 180f, yPos + 10f, 160f, 20f), Time.timeScale, 0f, 1f); Time.fixedDeltaTime = 0.02f * Time.timeScale; labelStyle.fontSize = Math.Max(guiTextSize, 20); } #endregion } }