using System; using UnityEngine; using UnityEngine.InputSystem; // ReSharper disable once CheckNamespace namespace BlueWaterProject { public class NewCombatPlayer : MonoBehaviour, IMovement3D { /*********************************************************************** * Definitions ***********************************************************************/ #region Class [Serializable] public class Components { public PlayerInput playerInput; public CapsuleCollider capsuleCollider; public Rigidbody rb; public Transform visualLook; public Animator animator; public Transform groundCheck; } [Serializable] public class CheckOption { [Tooltip("지면으로 체크할 레이어 설정")] public LayerMask groundLayerMask = -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.1f), Tooltip("지면 인식 허용 거리")] public float groundCheckThreshold = 0.01f; } [Serializable] public class MovementOption { [Range(1f, 10f), Tooltip("이동속도")] public float speed = 10f; [Range(1f, 3f), Tooltip("달리기 이동속도 증가 계수")] public float runningCoefficient = 1.5f; [Range(1f, 75f), Tooltip("등반 가능한 경사각")] public float maxSlopeAngle = 50f; [Range(0f, 4f), Tooltip("경사로 이동속도 변화율(가속/감속)")] public float slopeAccel = 1f; [Range(-9.81f, 0f), Tooltip("중력")] public float gravity = -9.81f; } [Serializable] public class CurrentState { public bool isMoving; public bool isRunning; public bool isGrounded; public bool isOnSteepSlope; // 등반 불가능한 경사로에 올라와 있음 public bool isForwardBlocked; // 전방에 장애물 존재 public bool isOutOfControl; // 제어 불가 상태 } [Serializable] public class CurrentValue { public Vector2 movementInput; public Vector3 worldMoveDirection; public Vector3 groundNormal; public Vector3 groundCross; public Vector3 horizontalVelocity; [Space] public float outOfControlDuration; [Space] public float groundDistance; public float groundSlopeAngle; // 현재 바닥의 경사각 public float groundVerticalSlopeAngle; // 수직으로 재측정한 경사각 public float forwardSlopeAngle; // 캐릭터가 바라보는 방향의 경사각 public float slopeAccel; // 경사로 인한 가속/감속 비율 [Space] public float gravity; } #endregion /*********************************************************************** * Variables ***********************************************************************/ #region Variables // [SerializeField] private Components components = new(); // [SerializeField] private CheckOption checkOptions = new(); // [SerializeField] private MovementOption moveOptions = new(); // [SerializeField] private CurrentState currentStates = new(); // [SerializeField] private CurrentValue currentValues = new(); [field: SerializeField] public Components PlayerComponents { get; private set; } = new(); [field: SerializeField] public CheckOption PlayerCheckOption { get; private set; } = new(); [field: SerializeField] public MovementOption PlayerMovementOption { get; private set; } = new(); [field: SerializeField] public CurrentState PlayerCurrentState { get; set; } = new(); [field: SerializeField] public CurrentValue PlayerCurrentValue { get; set; } = new(); private float capsuleRadiusDifferent; private float fixedDeltaTime; private float castRadius; // Sphere, Capsule 레이캐스트 반지름 private Vector3 CapsuleTopCenterPoint => new(transform.position.x, transform.position.y + PlayerComponents.capsuleCollider.height - PlayerComponents.capsuleCollider.radius, transform.position.z); private Vector3 CapsuleBottomCenterPoint => new(transform.position.x, transform.position.y + PlayerComponents.capsuleCollider.radius, transform.position.z); #endregion /*********************************************************************** * Unity Events ***********************************************************************/ #region Unity Events private void Start() { InitRigidbody(); InitCapsuleCollider(); } private void Update() { Move(); } private void FixedUpdate() { fixedDeltaTime = Time.fixedDeltaTime; CheckGround(); CheckForward(); UpdatePhysics(); UpdateValues(); CalculateMovements(); ApplyMovementsToRigidbody(); } #endregion /*********************************************************************** * Init Methods ***********************************************************************/ #region Init Methods private void InitRigidbody() { TryGetComponent(out PlayerComponents.rb); if (PlayerComponents.rb == null) { PlayerComponents.rb = gameObject.AddComponent(); } PlayerComponents.rb.constraints = RigidbodyConstraints.FreezeRotation; PlayerComponents.rb.interpolation = RigidbodyInterpolation.Interpolate; PlayerComponents.rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; PlayerComponents.rb.useGravity = false; } private void InitCapsuleCollider() { TryGetComponent(out PlayerComponents.capsuleCollider); if (PlayerComponents.capsuleCollider == null) { PlayerComponents.capsuleCollider = gameObject.AddComponent(); // 렌더러를 모두 탐색하여 높이 결정 var maxHeight = -1f; // // 1. SMR 확인 // var smrArr = GetComponentsInChildren(); // if (smrArr.Length > 0) // { // foreach (var smr in smrArr) // { // foreach (var vertex in smr.sharedMesh.vertices) // { // if(maxHeight < vertex.y) // maxHeight = vertex.y; // } // } // } // // 2. MR 확인 // else // { // var mfArr = GetComponentsInChildren(); // if (mfArr.Length > 0) // { // foreach (var mf in mfArr) // { // foreach (var vertex in mf.mesh.vertices) // { // if (maxHeight < vertex.y) // maxHeight = vertex.y; // } // } // } // } // 3. 캡슐 콜라이더 값 설정 if (maxHeight <= 0) { maxHeight = 1f; } var center = maxHeight * 0.5f; PlayerComponents.capsuleCollider.height = maxHeight; PlayerComponents.capsuleCollider.center = Vector3.up * center; PlayerComponents.capsuleCollider.radius = 0.5f; } var capsuleColliderRadius = PlayerComponents.capsuleCollider.radius; castRadius = capsuleColliderRadius * 0.9f; capsuleRadiusDifferent = capsuleColliderRadius - castRadius + 0.05f; } #endregion /*********************************************************************** * PlayerInput ***********************************************************************/ #region PlayerInput private void OnMove(InputValue value) { PlayerCurrentValue.movementInput = value.Get(); } #endregion /*********************************************************************** * IMovement3D interface ***********************************************************************/ #region IMovement3D interface public bool IsMoving() => PlayerCurrentState.isMoving; public bool IsGrounded() => PlayerCurrentState.isGrounded; public float GetDistanceFromGround() => PlayerCurrentValue.groundDistance; public void SetMovement(in Vector3 worldMoveDirection, bool isRunning) { PlayerCurrentValue.worldMoveDirection = worldMoveDirection; PlayerCurrentState.isMoving = worldMoveDirection.sqrMagnitude > 0.01f; PlayerCurrentState.isRunning = isRunning; } public void StopMoving() { PlayerCurrentValue.worldMoveDirection = Vector3.zero; PlayerCurrentState.isMoving = false; PlayerCurrentState.isRunning = false; } public void KnockBack(in Vector3 force, float time) { SetOutOfControl(time); PlayerComponents.rb.AddForce(force, ForceMode.Impulse); } public void SetOutOfControl(float time) { PlayerCurrentValue.outOfControlDuration = time; } #endregion /*********************************************************************** * Methods ***********************************************************************/ #region Methods private void Move() { // 이동하지 않는 경우, 미끄럼 방지 if (PlayerCurrentState.isMoving == false) { PlayerComponents.rb.velocity = new Vector3(0f, PlayerComponents.rb.velocity.y, 0f); return; } // 실제 이동 벡터 계산 var worldMove = new Vector3(PlayerCurrentValue.movementInput.x, 0, PlayerCurrentValue.movementInput.y).normalized; worldMove *= (PlayerMovementOption.speed) * (PlayerCurrentState.isRunning ? PlayerMovementOption.runningCoefficient : 1f); // Y축 속도는 유지하면서 XZ평면 이동 PlayerComponents.rb.velocity = new Vector3(worldMove.x, PlayerComponents.rb.velocity.y, worldMove.z); } /// 하단 지면 검사 private void CheckGround() { PlayerCurrentValue.groundDistance = float.MaxValue; PlayerCurrentValue.groundNormal = Vector3.up; PlayerCurrentValue.groundSlopeAngle = 0f; PlayerCurrentValue.forwardSlopeAngle = 0f; var cast = Physics.SphereCast(CapsuleBottomCenterPoint, castRadius, Vector3.down, out var hit, PlayerCheckOption.groundCheckDistance, PlayerCheckOption.groundLayerMask, QueryTriggerInteraction.Ignore); PlayerCurrentState.isGrounded = false; if (cast) { // 지면 노멀벡터 초기화 PlayerCurrentValue.groundNormal = hit.normal; // 현재 위치한 지면의 경사각 구하기(캐릭터 이동방향 고려) PlayerCurrentValue.groundSlopeAngle = Vector3.Angle(PlayerCurrentValue.groundNormal, Vector3.up); PlayerCurrentValue.forwardSlopeAngle = Vector3.Angle(PlayerCurrentValue.groundNormal, PlayerCurrentValue.worldMoveDirection) - 90f; PlayerCurrentState.isOnSteepSlope = PlayerCurrentValue.groundSlopeAngle >= PlayerMovementOption.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; //} PlayerCurrentValue.groundDistance = Mathf.Max(hit.distance - capsuleRadiusDifferent - PlayerCheckOption.groundCheckThreshold, 0f); PlayerCurrentState.isGrounded = (PlayerCurrentValue.groundDistance <= 0.0001f) && !PlayerCurrentState.isOnSteepSlope; GzUpdateValue(ref gzGroundTouch, hit.point); } // 월드 이동벡터 회전축 PlayerCurrentValue.groundCross = Vector3.Cross(PlayerCurrentValue.groundNormal, Vector3.up); } /// 전방 장애물 검사 : 레이어 관계 없이 trigger가 아닌 모든 장애물 검사 private void CheckForward() { bool cast = Physics.CapsuleCast(CapsuleBottomCenterPoint, CapsuleTopCenterPoint, castRadius, PlayerCurrentValue.worldMoveDirection + Vector3.down * 0.1f, out var hit, PlayerCheckOption.forwardCheckDistance, -1, QueryTriggerInteraction.Ignore); PlayerCurrentState.isForwardBlocked = false; if (cast) { float forwardObstacleAngle = Vector3.Angle(hit.normal, Vector3.up); PlayerCurrentState.isForwardBlocked = forwardObstacleAngle >= PlayerMovementOption.maxSlopeAngle; GzUpdateValue(ref gzForwardTouch, hit.point); } } private void UpdatePhysics() { // Custom Gravity, Jumping State if (PlayerCurrentState.isGrounded) { PlayerCurrentValue.gravity = 0f; } else { PlayerCurrentValue.gravity += fixedDeltaTime * PlayerMovementOption.gravity; } } private void UpdateValues() { // Out Of Control PlayerCurrentState.isOutOfControl = PlayerCurrentValue.outOfControlDuration > 0f; if (PlayerCurrentState.isOutOfControl) { PlayerCurrentValue.outOfControlDuration -= fixedDeltaTime; PlayerCurrentValue.worldMoveDirection = Vector3.zero; } } private void CalculateMovements() { if (PlayerCurrentState.isOutOfControl) { PlayerCurrentValue.horizontalVelocity = Vector3.zero; return; } // 0. 가파른 경사면에 있는 경우 : 꼼짝말고 미끄럼틀 타기 //if (State.isOnSteepSlope && Current.groundDistance < 0.1f) //{ // DebugMark(0); // Current.horizontalVelocity = // Quaternion.AngleAxis(90f - Current.groundSlopeAngle, Current.groundCross) * (Vector3.up * Current.gravity); // Com.rBody.velocity = Current.horizontalVelocity; // return; //} // 2. XZ 이동속도 계산 // 공중에서 전방이 막힌 경우 제한 (지상에서는 벽에 붙어서 이동할 수 있도록 허용) if (PlayerCurrentState.isForwardBlocked && !PlayerCurrentState.isGrounded) { PlayerCurrentValue.horizontalVelocity = Vector3.zero; } else // 이동 가능한 경우 : 지상 or 전방이 막히지 않음 { float speed = !PlayerCurrentState.isMoving ? 0f : !PlayerCurrentState.isRunning ? PlayerMovementOption.speed : PlayerMovementOption.speed * PlayerMovementOption.runningCoefficient; PlayerCurrentValue.horizontalVelocity = PlayerCurrentValue.worldMoveDirection * speed; } // 3. XZ 벡터 회전 // 지상이거나 지면에 가까운 높이 if (PlayerCurrentState.isGrounded || PlayerCurrentValue.groundDistance < PlayerCheckOption.groundCheckDistance) { if (PlayerCurrentState.isMoving && !PlayerCurrentState.isForwardBlocked) { // 경사로 인한 가속/감속 if (PlayerMovementOption.slopeAccel > 0f) { bool isPlus = PlayerCurrentValue.forwardSlopeAngle >= 0f; float absFsAngle = isPlus ? PlayerCurrentValue.forwardSlopeAngle : -PlayerCurrentValue.forwardSlopeAngle; float accel = PlayerMovementOption.slopeAccel * absFsAngle * 0.01111f + 1f; PlayerCurrentValue.slopeAccel = !isPlus ? accel : 1.0f / accel; PlayerCurrentValue.horizontalVelocity *= PlayerCurrentValue.slopeAccel; } // 벡터 회전 (경사로) PlayerCurrentValue.horizontalVelocity = Quaternion.AngleAxis(-PlayerCurrentValue.groundSlopeAngle, PlayerCurrentValue.groundCross) * PlayerCurrentValue.horizontalVelocity; } } GzUpdateValue(ref gzRotatedWorldMoveDirection, PlayerCurrentValue.horizontalVelocity * 0.2f); } /// 리지드바디 최종 속도 적용 private void ApplyMovementsToRigidbody() { if (PlayerCurrentState.isOutOfControl) { PlayerComponents.rb.velocity = new Vector3(PlayerComponents.rb.velocity.x, PlayerCurrentValue.gravity, PlayerComponents.rb.velocity.z); return; } PlayerComponents.rb.velocity = PlayerCurrentValue.horizontalVelocity + Vector3.up * (PlayerCurrentValue.gravity); } #endregion /*********************************************************************** * Gizmos, GUI ***********************************************************************/ #region . private Vector3 gzGroundTouch; private Vector3 gzForwardTouch; private Vector3 gzRotatedWorldMoveDirection; [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 (PlayerCurrentState.isForwardBlocked) { Gizmos.color = Color.blue; Gizmos.DrawSphere(gzForwardTouch, gizmoRadius); } Gizmos.color = Color.blue; Gizmos.DrawLine(gzGroundTouch - PlayerCurrentValue.groundCross, gzGroundTouch + PlayerCurrentValue.groundCross); Gizmos.color = Color.black; Gizmos.DrawLine(transform.position, transform.position + gzRotatedWorldMoveDirection); 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 GzUpdateValue(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 = PlayerCurrentValue.forwardSlopeAngle == -90f ? prevForwardSlopeAngle : PlayerCurrentValue.forwardSlopeAngle; var oldColor = GUI.color; GUI.color = new Color(0f, 0f, 0f, 0.5f); GUI.Box(new Rect(40, 40, 420, 260), ""); GUI.color = oldColor; GUILayout.BeginArea(new Rect(50, 50, 1000, 500)); GUILayout.Label($"Ground Height : {Mathf.Min(PlayerCurrentValue.groundDistance, 99.99f): 00.00}", labelStyle); GUILayout.Label($"Slope Angle(Ground) : {PlayerCurrentValue.groundSlopeAngle: 00.00}", labelStyle); GUILayout.Label($"Slope Angle(Forward) : {prevForwardSlopeAngle: 00.00}", labelStyle); GUILayout.Label($"Allowed Slope Angle : {PlayerMovementOption.maxSlopeAngle: 00.00}", labelStyle); GUILayout.Label($"Current Slope Accel : {PlayerCurrentValue.slopeAccel: 00.00}", labelStyle); GUILayout.Label($"Current Speed Mag : {PlayerCurrentValue.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 : {PlayerMovementOption.speed: 00.00}", RTLabelStyle); PlayerMovementOption.speed = GUI.HorizontalSlider(new Rect(sWidth - 200f, yPos + 10f, 180f, 20f), PlayerMovementOption.speed, 1f, 10f); yPos += 20f; GUI.Label(new Rect(sWidth - 350f, yPos, 150f, 30f), $"Max Slope : {PlayerMovementOption.maxSlopeAngle: 00}", RTLabelStyle); PlayerMovementOption.maxSlopeAngle = (int)GUI.HorizontalSlider( new Rect(sWidth - 200f, yPos + 10f, 180f, 20f), PlayerMovementOption.maxSlopeAngle, 1f, 75f); labelStyle.fontSize = Math.Max(guiTextSize, 20); } #endregion } }