using System; using System.Collections; using BehaviorDesigner.Runtime; using Sirenix.OdinInspector; using UnityEngine; using UnityEngine.AI; // ReSharper disable once CheckNamespace namespace BlueWaterProject { public abstract class Enemy : BaseCharacter, IDamageable, IAiView, IHelpCall { #region Properties and variables [Title("DrawGizmos")] [Tooltip("전체 Gizmos 그리기 여부")] [SerializeField] private bool isDrawGizmos = true; [ShowIf("@isDrawGizmos")] [Tooltip("타겟 인식 범위 그리기 여부")] [SerializeField] private bool isDrawViewRange = true; [ShowIf("@isDrawGizmos")] [Tooltip("이동제한 범위 그리기 여부")] [SerializeField] private bool isDrawDefenseRange = true; [ShowIf("@isDrawGizmos")] [Tooltip("Idle 상태에서 랜덤으로 이동하는 범위 그리기 여부")] [SerializeField] private bool isDrawRandomMoveRange = true; [ShowIf("@isDrawGizmos")] [Tooltip("타겟과의 상태 그리기 여부\n빨간색 = 공격 범위 밖\n파란색 = 공격 범위 안")] [SerializeField] private bool isDrawTargetRange = true; [field: Title("Stat")] [field: Tooltip("행동 타입 설정")] [field: SerializeField] public EBehaviorType BehaviorType { get; private set; } = EBehaviorType.DEFENDER; [field: Tooltip("최대 체력 설정")] [field: SerializeField] public float MaxHp { get; private set; } = 100f; [field: Tooltip("현재 체력")] [field: SerializeField] public float CurrentHp { get; private set; } [field: Tooltip("이동 속도 설정")] [field: SerializeField] public float MoveSpd { get; private set; } = 5f; [field: Tooltip("공격력 설정")] [field: SerializeField] public float Atk { get; private set; } = 10f; [field: Tooltip("공격 속도(다음 공격 주기)\nAtkCooldown = 2f (2초마다 1번 공격)")] [field: SerializeField] public float AtkCooldown { get; private set; } = 1f; [field: Tooltip("공격 사거리 설정")] [field: SerializeField] public float AtkRange { get; private set; } = 1.5f; [field: ShowIf("@BehaviorType == EBehaviorType.DEFENDER || BehaviorType == EBehaviorType.KEEPER")] [field: Tooltip("이동 제한 범위 설정")] [field: SerializeField] public float DefenseRange { get; private set; } = 20f; [field: Tooltip("Idle 상태에서 랜덤으로 이동 여부")] [field: SerializeField] public bool IsRandomMove { get; private set; } = true; [field: ShowIf("@IsRandomMove")] [field: Tooltip("Idle 상태에서 이동하는 범위 설정")] [field: SerializeField] public float RandomMoveRange { get; private set; } = 5f; [field: Title("Data")] [field: DisableIf("@true")] [field: SerializeField] public Vector3 DefensePos { get; set; } [field: DisableIf("@true")] [field: SerializeField] public bool IsCombated { get; set; } [field: DisableIf("@true")] [field: SerializeField] public bool BeAttackedInIdle { get; set; } [DisableIf("@true")] [SerializeField] private bool beAttacked; protected bool isAttacking; // Component protected Rigidbody rb; public Collider MyCollider { get; private set; } public NavMeshAgent Agent { get; private set; } protected BehaviorTree bt; protected Animator myAnimator; // Hash protected static readonly int RunStateHash = Animator.StringToHash("RunState"); // Const private static readonly WaitForSeconds BeAttackedWaitTime = new(0.3f); #endregion #region Unity built-in methods protected override void OnDrawGizmosSelected() { base.OnDrawGizmosSelected(); if (!isDrawGizmos) return; Vector3 myCenterPos; Vector3 defensePos; if (Application.isPlaying) { myCenterPos = MyCollider.bounds.center; defensePos = DefensePos; } else { myCenterPos = GetComponent().bounds.center; defensePos = transform.position; } switch (BehaviorType) { case EBehaviorType.NONE: break; case EBehaviorType.STRIKER: break; case EBehaviorType.DEFENDER: case EBehaviorType.KEEPER: if (isDrawDefenseRange) { Gizmos.color = Color.blue; Gizmos.DrawWireSphere(defensePos, DefenseRange); } break; default: throw new ArgumentOutOfRangeException(); } if (isDrawRandomMoveRange) { Gizmos.color = Color.green; Gizmos.DrawWireSphere(defensePos, RandomMoveRange); } if (isDrawViewRange) { Gizmos.color = Color.red; Gizmos.DrawWireSphere(myCenterPos, ViewRadius); } if (UseHelpCall && IsDrawHelpCallRange) { Gizmos.color = Color.magenta; Gizmos.DrawWireSphere(myCenterPos, HelpCallRange); } if (!Target || !isDrawTargetRange) return; var targetToDistance = Vector3.Distance(myCenterPos, Target.bounds.center); Gizmos.color = targetToDistance <= AtkRange ? Color.blue : Color.red; Gizmos.DrawLine(myCenterPos, Target.bounds.center); } protected override void Reset() { base.Reset(); isDrawGizmos = true; isDrawViewRange = true; isDrawDefenseRange = true; isDrawRandomMoveRange = true; isDrawTargetRange = true; BehaviorType = EBehaviorType.DEFENDER; MaxHp = 100f; MoveSpd = 5f; AtkCooldown = 1f; AtkRange = 1.5f; DefenseRange = 20f; IsRandomMove = true; RandomMoveRange = 5f; ViewRadius = 15f; UseHelpCall = false; HelpLayer = LayerMask.GetMask("Enemy"); HelpCallRange = 15f; } protected override void Awake() { base.Awake(); rb = GetComponent(); MyCollider = GetComponent(); Agent = GetComponent(); bt = GetComponent(); myAnimator = transform.Find("UnitRoot")?.GetComponent(); if (myAnimator == null) { print("UnitRoot오브젝트를 찾을 수 없거나, Animator컴포넌트가 존재하지 않습니다."); } } protected override void Start() { base.Start(); HelpLayer = LayerMask.GetMask("Enemy"); TargetLayer = LayerMask.GetMask("Player"); Agent.updateRotation = false; DefensePos = transform.position; SetAgentSpeed(ESpeedType.DEFAULT); SetCurrentHp(MaxHp); } #endregion #region Interface // IDamageable public virtual void TakeDamage(float attackerPower, float attackerShieldPenetrationRate = default, Vector3? attackPos = null) { IsCombated = true; if (!Target) { BeAttackedInIdle = true; bt.SendEvent("BeAttackedInIdle", attackPos); } else { if (UseHelpCall) { HelpCall(); } } var changeHp = Mathf.Max(CurrentHp - attackerPower, 0); SetCurrentHp(changeHp); // 죽었는지 체크 if (changeHp == 0f) { return; } StartCoroutine(nameof(BeAttacked)); } // IAiView [field: Title("IAiView")] [field: SerializeField] public float ViewRadius { get; set; } = 15f; [field: SerializeField] public Collider[] Targets { get; set; } = new Collider[MAX_COLLIDERS]; [field: SerializeField] public Collider Target { get; set; } [field: SerializeField] public LayerMask TargetLayer { get; set; } private const int MAX_COLLIDERS = 30; public void FindNearestTargetInRange(bool targetIsTrigger = true) { Array.Clear(Targets, 0, MAX_COLLIDERS); var myCenterPos = MyCollider.bounds.center; var numResults = Physics.OverlapSphereNonAlloc(myCenterPos, ViewRadius, Targets, TargetLayer, targetIsTrigger ? QueryTriggerInteraction.Collide : QueryTriggerInteraction.Ignore); if (numResults <= 0) { SetTarget(null); return; } var nearestDistance = ViewRadius * ViewRadius; Collider nearestTargetCollider = null; for (var i = 0; i < numResults; i++) { var distanceSqrToTarget = (myCenterPos - Targets[i].bounds.center).sqrMagnitude; if (distanceSqrToTarget >= nearestDistance) continue; nearestDistance = distanceSqrToTarget; nearestTargetCollider = Targets[i]; } SetTarget(nearestTargetCollider); } // IHelpCall [field: Title("IHelpCall")] [field: Tooltip("주변 아군에게 도움 요청")] [field: SerializeField] public bool UseHelpCall { get; set; } [field: ShowIf("@UseHelpCall && isDrawGizmos")] [field: Tooltip("도움 요청 범위 그리기 여부")] [field: SerializeField] public bool IsDrawHelpCallRange { get; set; } [field: ShowIf("@UseHelpCall")] [field: Tooltip("도움 요청 범위 설정")] [field: SerializeField] public LayerMask HelpLayer { get; set; } [field: ShowIf("@UseHelpCall")] [field: Tooltip("도움 요청 범위 설정")] [field: SerializeField] public float HelpCallRange { get; set; } = 15f; [field: ShowIf("@UseHelpCall")] [field: Tooltip("도움 요청 받은 아군 목록")] [field: SerializeField] public Collider[] HelpTargets { get; set; } = new Collider[MAX_COLLIDERS]; #endregion #region Custom methods private IEnumerator BeAttacked() { beAttacked = true; myAnimator.SetFloat(RunStateHash, 1f); yield return BeAttackedWaitTime; beAttacked = false; } public bool IsTargetWithinRange() { var attackInRange = Vector3.Distance(MyCollider.bounds.center, Target.bounds.center) <= AtkRange; return attackInRange; } public bool GoOutOfBounds() { if (BehaviorType != EBehaviorType.DEFENDER && BehaviorType != EBehaviorType.KEEPER) return false; var defensePosInRange = Vector3.Distance(transform.position, DefensePos) <= DefenseRange; return !defensePosInRange; } public void MoveTarget(Vector3 targetPos, ESpeedType speedType, float stopDistance) { switch (BehaviorType) { case EBehaviorType.NONE: print("BehaviorType == NONE error"); break; case EBehaviorType.STRIKER: case EBehaviorType.DEFENDER: break; case EBehaviorType.KEEPER: return; default: throw new ArgumentOutOfRangeException(); } if (Vector3.Distance(Agent.destination, targetPos) < 0.1f) return; SetAgentSpeed(speedType); Agent.stoppingDistance = stopDistance; Agent.isStopped = false; Agent.SetDestination(targetPos); } public void HelpCall(bool targetIsTrigger = true) { Array.Clear(HelpTargets, 0, MAX_COLLIDERS); var myCenterPos = MyCollider.bounds.center; var numResults = Physics.OverlapSphereNonAlloc(myCenterPos, HelpCallRange, HelpTargets, HelpLayer, targetIsTrigger ? QueryTriggerInteraction.Collide : QueryTriggerInteraction.Ignore); for (var i = 0; i < numResults; i++) { var iHelpCall = HelpTargets[i].GetComponent(); if (iHelpCall == null || iHelpCall.Target != null) continue; iHelpCall.SetTarget(Target); } } private void SetAgentSpeed(ESpeedType speedType) { switch (speedType) { case ESpeedType.NONE: print("speedType == NONE error"); break; case ESpeedType.DEFAULT: Agent.speed = MoveSpd; break; case ESpeedType.SLOW: Agent.speed = MoveSpd * 0.5f; break; case ESpeedType.FAST: Agent.speed = MoveSpd * 2f; break; default: throw new ArgumentOutOfRangeException(nameof(speedType), speedType, null); } } public void SetTarget(Collider value) { Target = value; if (value != null) { IsCombated = true; BeAttackedInIdle = false; } } private void SetCurrentHp(float value) => CurrentHp = value; #endregion } }