using System; using System.Collections; using Sirenix.OdinInspector; using UnityEngine; using UnityEngine.InputSystem; // ReSharper disable once CheckNamespace namespace BlueWaterProject { public class CombatPlayerController : MonoBehaviour, IDamageable { /*********************************************************************** * Definitions ***********************************************************************/ #region Class [Serializable] public class Components { public PlayerInput playerInput; public Transform visualLook; public SpriteRenderer spriteRenderer; public Animator animator; public PhysicsMovement movement; public HpSlider hpSlider; } [Serializable] public class CharacterOption { [Range(0f, 1000f), Tooltip("최대 체력")] public float maxHp = 100f; [Title("공격")] [Range(1, 21), Tooltip("한 번에 공격 가능한 개체 수")] public int maxHitNum = 10; [Range(0f, 100f), Tooltip("첫 번째 공격 데미지")] public float firstAttackDamage = 10f; [Range(0f, 100f), Tooltip("두 번째 공격 데미지")] public float secondAttackDamage = 15f; [Range(0.1f, 2f), Tooltip("콤보 공격 포함 총 걸리는 시간")] public float attackTime = 0.7f; [Range(0.1f, 5f), Tooltip("공격 범위 사거리(반지름)")] public float attackRange = 1.5f; [Range(0f, 360f), Tooltip("공격 범위 각도")] public float attackAngle = 180f; [Tooltip("공격할 레이어 설정")] public LayerMask targetLayer = -1; [Tooltip("땅 레이어 설정")] public LayerMask groundLayer; } [Serializable] [DisableIf("@true")] public class CurrentState { public bool useMouseAttack = true; public bool isInvincibility; public bool isAttacking; public bool isComboPossible; public bool isComboAttacking; public bool isUsingSkill; } [Serializable] public class CurrentValue { [Tooltip("현재 체력")] public float currentHp; [Tooltip("최근에 공격 받은 충돌체 배열")] public Collider[] hitColliders; } #endregion /*********************************************************************** * Variables ***********************************************************************/ #region Variables [field: SerializeField] public Components MyComponents { get; private set; } [field: SerializeField] public CharacterOption MyCharacterOption { get; private set; } [field: SerializeField] public CurrentState MyCurrentState { get; set; } [field: SerializeField] public CurrentValue MyCurrentValue { get; set; } [Title("애니메이션")] [SerializeField] private float comboAttackSpeed = 1f; [SerializeField] private float[] comboAttackTiming = new[] { 0.14f, 0.77f }; [SerializeField] private float checkComboAttackTiming = 0.49f; [Title("효과")] [SerializeField] private float comboAttackHitStopDuration = 0.2f; [SerializeField] private ParticleSystem swordHit; public string mainSkillName; public GameObject MainSkillObject { get; private set; } private ISkill mainSkill; public static readonly int IsMovingHash = Animator.StringToHash("isMoving"); public static readonly int XDirectionHash = Animator.StringToHash("xDirection"); public static readonly int ZDirectionHash = Animator.StringToHash("zDirection"); public static readonly int IsAttackingHash = Animator.StringToHash("isAttacking"); public static readonly int IsActivateMainSkillHash = Animator.StringToHash("isActivateMainSkill"); private static readonly int IsHitHash = Shader.PropertyToID("_IsHit"); #endregion /*********************************************************************** * Unity Events ***************************************************************m********/ #region Unity Events private void OnEnable() { MyComponents.playerInput.actions.FindAction("Attack").performed += OnAttackEvent; } private void OnDisable() { MyComponents.playerInput.actions.FindAction("Attack").performed -= OnAttackEvent; } private void OnDestroy() { if (MyComponents.hpSlider) { Destroy(MyComponents.hpSlider.gameObject); } } private void Start() { InitComponents(); InitSkills(); InitStartValue(); } private void Update() { MoveAnimation(); FlipVisualLook(MyComponents.movement.GetPreviousMoveDirection().x); } #endregion /*********************************************************************** * Init Methods ***********************************************************************/ #region Unity Events private void InitComponents() { if (!TryGetComponent(out MyComponents.movement)) { MyComponents.movement = gameObject.AddComponent(); } MyComponents.movement.SetAnimator(MyComponents.animator); } private void InitSkills() { MainSkillObject = SkillManager.Inst.InstantiateSkill(mainSkillName).gameObject; mainSkill = MainSkillObject.GetComponent(); if (mainSkill != null) { var myCollider = MyComponents.movement.MyComponents.capsuleCollider; var myRb = MyComponents.movement.MyComponents.rb; var myVisualLook = MyComponents.visualLook; var myAnimator = MyComponents.animator; var targetLayer = MyCharacterOption.targetLayer; var ui = CombatUiManager.Inst.MainSkillUi; ui.gameObject.SetActive(true); mainSkill.SkillInputData.InitInputData(transform, myCollider, myRb, null, myVisualLook, myAnimator, null, targetLayer, ui, null, null); mainSkill.InitData(); } } private void InitStartValue() { MyCurrentValue.hitColliders = new Collider[MyCharacterOption.maxHitNum]; MyComponents.hpSlider = Instantiate(MyComponents.hpSlider, Vector3.zero, Quaternion.identity, CombatUiManager.Inst.WorldSpaceCanvas.transform); MyComponents.hpSlider.SetHpSlider(transform, MyCharacterOption.maxHp); SetCurrentHp(MyCharacterOption.maxHp); } #endregion /*********************************************************************** * Player Input ***********************************************************************/ #region Unity Events private void OnAttackEvent(InputAction.CallbackContext context) { if (MyCurrentState.isAttacking && !MyCurrentState.isComboPossible) return; var control = context.control; if (control.name.Equals("leftButton")) { MyCurrentState.useMouseAttack = true; } else if (control.name.Equals("k")) { MyCurrentState.useMouseAttack = false; } if (MyCurrentState.isComboPossible) { MyCurrentState.isComboAttacking = true; return; } StartCoroutine(ComboAttackCoroutine()); } private void OnActivateMainSkill() { if (MyCurrentState.isAttacking || GetIsDashing()) return; mainSkill.SkillInputData.StartPosition = GetCurrentPosition(); if (mainSkill.EnableSkill()) { SetEnableMoving(false); MyComponents.animator.SetBool(IsActivateMainSkillHash, true); mainSkill.ActivateSkill(); } } #endregion /*********************************************************************** * Interfaces ***********************************************************************/ #region Interfaces // IDamageable public void TakeDamage(float attackerPower, Vector3? attackPos = null) { if (MyCurrentState.isInvincibility) return; var changeHp = Mathf.Max(MyCurrentValue.currentHp - attackerPower, 0); SetCurrentHp(changeHp); MyComponents.hpSlider.UpdateHpSlider(changeHp); // 죽었는지 체크 if (changeHp == 0f) { Die(); return; } StartCoroutine(nameof(FlashWhiteCoroutine)); } public void Die() { print("Combat Player Die"); } public float GetCurrentHp() => MyCurrentValue.currentHp; #endregion /*********************************************************************** * Methods ***********************************************************************/ #region Methods private IEnumerator ComboAttackCoroutine() { SetEnableMoving(false); if (MyCurrentState.useMouseAttack) { var mousePos = Mouse.current.position.ReadValue(); var ray = CameraManager.Inst.MainCam.ScreenPointToRay(mousePos); if (!Physics.Raycast(ray, out var hit, float.MaxValue, MyCharacterOption.groundLayer)) { CancelComboAttack(); yield break; } var attackDirection = (hit.point - GetCurrentPosition()).normalized; attackDirection.y = 0f; SetPreviousMoveDirection(attackDirection); } MyComponents.animator.SetBool(IsAttackingHash, true); var elapsedTime = 0f; while (!MyComponents.animator.GetCurrentAnimatorStateInfo(0).IsName("ComboAttack")) { elapsedTime += Time.deltaTime; if (elapsedTime >= 2f) { yield break; } yield return null; } var animationLength = MyComponents.animator.GetCurrentAnimatorStateInfo(0).length; var attackTimingNormalized = new[]{ comboAttackTiming[0] / animationLength, comboAttackTiming[1] / animationLength }; var checkComboAttackNormalized = checkComboAttackTiming / animationLength; var isAttacked = false; var isComboAttacked = false; MyComponents.animator.speed = comboAttackSpeed; SetIsAttacking(true); SetIsComboPossible(true); while (MyComponents.animator.GetCurrentAnimatorStateInfo(0).IsName("ComboAttack") && MyComponents.animator.GetCurrentAnimatorStateInfo(0).normalizedTime < 1f) { if (!isAttacked) { if (isComboAttacked && MyComponents.animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= attackTimingNormalized[1]) { MoveToCurrentDirection(10f); AttackTiming(MyCharacterOption.secondAttackDamage); isAttacked = true; } else if (!isComboAttacked && MyComponents.animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= attackTimingNormalized[0]) { MoveToCurrentDirection(1f); AttackTiming(MyCharacterOption.firstAttackDamage); isAttacked = true; } } if (MyComponents.animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= checkComboAttackNormalized) { if (!MyCurrentState.isComboAttacking) { CancelComboAttack(); yield break; } if (!isComboAttacked && MyCurrentState.isComboAttacking) { isAttacked = false; isComboAttacked = true; } } yield return null; } CancelComboAttack(); } public void AttackTiming(float damage) { var attackDirection = MyComponents.movement.GetPreviousMoveDirection(); var size = Physics.OverlapSphereNonAlloc(transform.position, MyCharacterOption.attackRange, MyCurrentValue.hitColliders, MyCharacterOption.targetLayer, QueryTriggerInteraction.Collide); for (var i = 0; i < size; i++) { var targetDirection = (MyCurrentValue.hitColliders[i].transform.position - transform.position).normalized; var angleBetween = Vector3.Angle(attackDirection, targetDirection); if (angleBetween >= MyCharacterOption.attackAngle * 0.5f) continue; if (MyCurrentValue.hitColliders[i].gameObject.layer == LayerMask.NameToLayer("Enemy")) { var iDamageable = MyCurrentValue.hitColliders[i].transform.GetComponent(); if (iDamageable != null) { iDamageable.TakeDamage(damage); var closestPoint = MyCurrentValue.hitColliders[i].ClosestPoint(transform.position); //var spawnPosition = closestPoint + Random.insideUnitSphere * 0.2f; Instantiate(swordHit, closestPoint, Quaternion.identity); } if (MyCurrentState.isComboAttacking) { VisualFeedbackManager.Inst.TriggerHitStop(comboAttackHitStopDuration); } } else if (MyCurrentValue.hitColliders[i].gameObject.layer == LayerMask.NameToLayer("Skill") && MyCurrentValue.hitColliders[i].CompareTag("DestructiveSkill")) { var iDamageable = MyCurrentValue.hitColliders[i].transform.GetComponent(); iDamageable?.TakeDamage(damage); } } } private void CancelComboAttack() { SetIsComboPossible(false); SetIsComboAttacking(false); SetIsAttacking(false); SetEnableMoving(true); MyComponents.animator.SetBool(IsAttackingHash, false); MyComponents.animator.speed = 1f; } private void MoveAnimation() { var isMoving = MyComponents.movement.GetIsMoving(); var previousDirection = MyComponents.movement.GetPreviousMoveDirection(); MyComponents.animator.SetBool(IsMovingHash, isMoving); if (isMoving) { MyComponents.animator.SetFloat(XDirectionHash, previousDirection.x); MyComponents.animator.SetFloat(ZDirectionHash, previousDirection.z); } } private void FlipVisualLook(float previousDirectionX) { var localScale = MyComponents.visualLook.localScale; localScale.x = previousDirectionX switch { > 0.01f => Mathf.Abs(localScale.x), < -0.01f => -Mathf.Abs(localScale.x), _ => localScale.x }; MyComponents.visualLook.localScale = localScale; } private IEnumerator FlashWhiteCoroutine() { var spriteRenderer = MyComponents.spriteRenderer; for (var i = 0; i < 5; i++) { spriteRenderer.material.SetInt(IsHitHash, 1); yield return new WaitForSeconds(0.05f); spriteRenderer.material.SetInt(IsHitHash, 0); yield return new WaitForSeconds(0.05f); } } 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(); } [Button("현재 체력 변경")] private void SetCurrentHp(float value) { MyCurrentValue.currentHp = value; if (MyCurrentValue.currentHp <= 30f) { CameraManager.Inst.CombatCamera.LowHpVignette(); } else { CameraManager.Inst.CombatCamera.DefaultHpVignette(); } } public void SetEnableMoving(bool value) => MyComponents.movement.SetEnableMoving(value); public void Move(Vector3 velocity) => MyComponents.movement.Move(velocity); public void MoveToCurrentDirection(float speed) => MyComponents.movement.MoveToCurrentDirection(speed); public bool GetIsAttacking() => MyCurrentState.isAttacking; public bool GetIsComboAttacking() => MyCurrentState.isComboAttacking; public bool GetIsDashing() => MyComponents.movement.GetIsDashing(); public float GetDashCooldown() => MyComponents.movement.GetDashCooldown(); public float GetDashTime() => MyComponents.movement.GetDashTime(); public float GetAttackTime() => MyCharacterOption.attackTime; public void SetIsAttacking(bool value) => MyCurrentState.isAttacking = value; public void SetIsComboAttacking(bool value) => MyCurrentState.isComboAttacking = value; public void SetIsComboPossible(bool value) => MyCurrentState.isComboPossible = value; public void SetIsDashing(bool value) => MyComponents.movement.SetIsDashing(value); public void SetEnableDashing(bool value) => MyComponents.movement.SetEnableDashing(value); public Vector3 GetCurrentPosition() => MyComponents.movement.GetCurrentPosition(); public void SetIsInvincibility(bool value) => MyCurrentState.isInvincibility = value; public void SetIsTrigger(bool value) => MyComponents.movement.SetIsTrigger(value); public void SetUseGravity(bool value) => MyComponents.movement.SetUseGravity(value); public void SetPreviousMoveDirection(Vector3 value) { MyComponents.movement.SetPreviousMoveDirection(value); if (value != Vector3.zero) { MyComponents.animator.SetFloat(XDirectionHash, value.x); MyComponents.animator.SetFloat(ZDirectionHash, value.z); } } #endregion } }