using System; using System.Collections; using Sirenix.OdinInspector; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.Serialization; // ReSharper disable once CheckNamespace namespace BlueWaterProject { public class CombatPlayer : MonoBehaviour, IDamageable { [Title("초기화 방식")] [SerializeField] private bool autoInit = true; [Title("캐릭터 변수")] [SerializeField] private float maxHp = 100f; [SerializeField] private float currentHp; [SerializeField] private float moveSpeed = 5f; [SerializeField] private float maxSlopeAngle = 50f; [field: Title("대쉬")] [field: SerializeField] public float DashForce { get; set; } = 20f; [field: SerializeField] public float DashCooldown { get; set; } = 0.5f; [field: DisableIf("@true")] [field: SerializeField] public bool IsDashing { get; set; } [field: DisableIf("@true")] [field: SerializeField] public bool EnableDash { get; set; } = true; [field: Title("공격")] [field: SerializeField] public int MaxHitNum { get; set; } = 10; [field: SerializeField] public float AttackDamage { get; set; } = 10f; [field: SerializeField] public float AttackRange { get; set; } = 1.5f; [field: SerializeField] public float AttackAngle { get; set; } = 180f; [field: SerializeField] public float ComboTime { get; set; } = 0.5f; [field: SerializeField] public LayerMask TargetLayer { get; set; } [field: DisableIf("@true")] [field: SerializeField] public bool IsAttacking { get; set; } [field: DisableIf("@true")] [field: SerializeField] public bool IsComboAttacking { get; set; } [field: DisableIf("@true")] [field: SerializeField] public bool IsComboPossible { get; set; } [Title("컴포넌트")] [SerializeField] private PlayerInput playerInput; [field: SerializeField] public Rigidbody Rb { get; set; } [SerializeField] private Transform visualLook; [field: SerializeField] public Animator Animator { get; set; } [SerializeField] private Transform groundCheck; [Title("Extensions Data")] [SerializeField] private float nextFrameCoefficient = 3f; private Vector2 movementInput; private Vector3 currentDirection = Vector3.back; public Vector3 PreviousDirection { get; set; } = Vector3.back; [field: SerializeField] public Collider[] HitColliders { get; set; } private RaycastHit slopeHit; private int groundLayer; private bool isOnSlope; private bool isGrounded; private const float RAY_DISTANCE = 3f; private static readonly int XDirectionHash = Animator.StringToHash("xDirection"); private static readonly int ZDirectionHash = Animator.StringToHash("zDirection"); private static readonly int IsMovingHash = Animator.StringToHash("isMoving"); public readonly int isDashingHash = Animator.StringToHash("isDashing"); public readonly int isAttackingHash = Animator.StringToHash("isAttacking"); private void OnDrawGizmosSelected() { var lossyScale = transform.lossyScale; var boxSize = new Vector3(lossyScale.x, 0.4f, lossyScale.z * 0.5f); Gizmos.color = IsGrounded() ? Color.blue : Color.red; Gizmos.DrawWireCube(groundCheck.position, boxSize); } [Button("셋팅 초기화")] private void Init() { playerInput = GetComponent(); Rb = GetComponent(); visualLook = transform.Find("VisualLook"); Animator = visualLook.GetComponent(); groundCheck = transform.Find("GroundCheck"); } private void Awake() { if (autoInit) { Init(); } } private void Start() { HitColliders = new Collider[MaxHitNum]; groundLayer = 1 << LayerMask.NameToLayer("Ground"); SetCurrentHp(maxHp); } private void OnEnable() { playerInput.actions.FindAction("Attack").performed += OnAttackEvent; } private void OnDisable() { playerInput.actions.FindAction("Attack").performed -= OnAttackEvent; } private void Update() { var isMoving = currentDirection.magnitude > 0.01f; if (isMoving) { PreviousDirection = currentDirection.normalized; Animator.SetFloat(XDirectionHash, PreviousDirection.x); Animator.SetFloat(ZDirectionHash, PreviousDirection.z); } Animator.SetBool(IsMovingHash, isMoving); var localScale = visualLook.localScale; localScale.x = PreviousDirection.x switch { > 0.01f => Mathf.Abs(localScale.x), < -0.01f => -Mathf.Abs(localScale.x), _ => localScale.x }; visualLook.localScale = localScale; isOnSlope = IsOnSlope(); isGrounded = IsGrounded(); } private void FixedUpdate() { HandleMovement(); } public void TakeDamage(float attackerPower, Vector3? attackPos = null) { if (IsDashing) return; var changeHp = Mathf.Max(currentHp - attackerPower, 0); SetCurrentHp(changeHp); if (InIslandCamera.Inst.InIslandCam) { VisualFeedbackManager.Inst.CameraShake(InIslandCamera.Inst.InIslandCam); } // 죽었는지 체크 if (changeHp == 0f) { Die(); return; } } public void Die() { print("Combat Player Die"); } private void OnMove(InputValue value) { movementInput = value.Get(); } public void OnDash() { if (!EnableDash || IsDashing) return; Animator.SetBool(isDashingHash, true); } private void OnAttackEvent(InputAction.CallbackContext context) { if (IsAttacking && !IsComboPossible) return; // var control = context.control; // // if (control.name.Equals("leftButton")) // { // UseMouseAttack = true; // } // else if (control.name.Equals("k")) // { // UseMouseAttack = false; // } if (IsComboPossible) { IsComboAttacking = true; return; } Animator.SetBool(isAttackingHash, true); } private void HandleMovement() { currentDirection = new Vector3(movementInput.x, 0, movementInput.y).normalized; //var currentDirection = IsDashing ? PreviousDirection : currentMovement.normalized; if (isOnSlope && isGrounded) { currentDirection = Vector3.ProjectOnPlane(currentDirection, slopeHit.normal).normalized; } var moveValue = IsDashing ? DashForce : moveSpeed; var newPosition = Rb.position + currentDirection * (moveValue * Time.fixedDeltaTime); Rb.MovePosition(newPosition); // var currentMovement = new Vector3(movementInput.x, 0, movementInput.y); // var currentDirection = IsDashing ? PreviousDirection : currentMovement.normalized; // var isNextFrameInAngle = CalculateNextFrameGroundAngle(currentDirection) < maxSlopeAngle; // var velocity = isNextFrameInAngle ? currentDirection : Vector3.zero; // var gravity = Vector3.down * Mathf.Abs(Rb.velocity.y); // // if (IsOnSlope() && IsGrounded()) // { // velocity = Vector3.ProjectOnPlane(currentDirection, slopeHit.normal).normalized; // gravity = Vector3.zero; // Rb.useGravity = false; // } // else // { // Rb.useGravity = true; // } // // var moveValue = IsDashing ? DashForce : moveSpeed; // Rb.velocity = velocity * moveValue + gravity; } public bool IsOnSlope() { var ray = new Ray(transform.position, Vector3.down); if (Physics.Raycast(ray, out slopeHit, RAY_DISTANCE, groundLayer)) { var angle = Vector3.Angle(Vector3.up, slopeHit.normal); Debug.DrawRay(transform.position, Vector3.down, angle != 0f && angle < maxSlopeAngle ? Color.blue : Color.red); return angle != 0f && angle < maxSlopeAngle; } Debug.DrawRay(transform.position, Vector3.down, Color.red); return false; } private bool IsGrounded() { var lossyScale = transform.lossyScale; var boxSize = new Vector3(lossyScale.x, 0.4f, lossyScale.z * 0.5f); return Physics.CheckBox(groundCheck.position, boxSize, Quaternion.identity, groundLayer); } private float CalculateNextFrameGroundAngle(Vector3 direction) { var nextFramePlayerPosition = transform.position + direction * (nextFrameCoefficient * moveSpeed * Time.fixedDeltaTime); if (Physics.Raycast(nextFramePlayerPosition, Vector3.down, out var hitInfo, RAY_DISTANCE, groundLayer)) { if (Vector3.Angle(Vector3.up, hitInfo.normal) > maxSlopeAngle) { Debug.DrawRay(hitInfo.point, hitInfo.normal, Color.red); } else { Debug.DrawRay(hitInfo.point, hitInfo.normal, Color.cyan); } Debug.DrawRay(nextFramePlayerPosition, Vector3.down, Color.green); return Vector3.Angle(Vector3.up, hitInfo.normal); } Debug.DrawRay(nextFramePlayerPosition, Vector3.down, Color.magenta); return 0; } public void AttackTiming() { var attackDirection = PreviousDirection; Array.Clear(HitColliders, 0, MaxHitNum); var size = Physics.OverlapSphereNonAlloc(transform.position, AttackRange, HitColliders, TargetLayer); for (var i = 0; i < size; i++) { var targetDirection = (HitColliders[i].transform.position - transform.position).normalized; var angleBetween = Vector3.Angle(attackDirection, targetDirection); if (angleBetween >= AttackAngle * 0.5f) continue; if (HitColliders[i].gameObject.layer == LayerMask.NameToLayer("Enemy")) { var iDamageable = HitColliders[i].transform.GetComponent(); iDamageable.TakeDamage(AttackDamage); VisualFeedbackManager.Inst.TriggerHitStop(0.1f); } else if (HitColliders[i].gameObject.layer == LayerMask.NameToLayer("Skill") && HitColliders[i].CompareTag("DestructiveSkill")) { var iDamageable = HitColliders[i].transform.GetComponent(); iDamageable.TakeDamage(AttackDamage); } } } public void CoolDown(float waitTime, Action onCooldownComplete = null) { StartCoroutine(CoolDownCoroutine(waitTime, onCooldownComplete)); } private IEnumerator CoolDownCoroutine(float waitTime, Action onCooldownComplete = null) { var time = 0f; while (time <= waitTime) { time += Time.deltaTime; yield return null; } onCooldownComplete?.Invoke(); } private void SetCurrentHp(float value) { currentHp = value; } public RaycastHit GetSlopeHit() => slopeHit; } }