using System; using System.Collections; using BehaviorDesigner.Runtime; using Sirenix.OdinInspector; using UnityEngine; using UnityEngine.AI; using UnityEngine.UI; // ReSharper disable once CheckNamespace namespace BlueWaterProject { public abstract class Enemy : BaseCharacter, IDamageable, IAiView, IHelpCall, IDroppable { #region Properties and variables // DrawGizmos [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; // Stat [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; set; } = 5f; [field: Tooltip("공격력 설정")] [field: SerializeField] public float Atk { get; private set; } = 10f; [field: Tooltip("공격 속도(다음 공격 주기)\nAtkCooldown = 2f (2초마다 1번 공격)")] [field: OnValueChanged("SetAtkWaitCooldown")] [field: SerializeField] public float AtkCooldown { get; private set; } = 1f; [field: Tooltip("공격 사거리 설정")] [field: SerializeField] public float AtkRange { get; 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; set; } = true; [field: ShowIf("@IsRandomMove")] [field: Tooltip("Idle 상태에서 이동하는 범위 설정")] [field: SerializeField] public float RandomMoveRange { get; set; } = 5f; // HpSlider [Title("HpSlider")] [SerializeField] private bool useHpSlider = true; [ShowIf("@useHpSlider")] [Required("HpSlider 프리팹을 넣어주세요.")] [SerializeField] private GameObject hpSliderPrefab; [ShowIf("@useHpSlider")] [SerializeField] private Vector3 hpSliderOffset = Vector3.up; [ShowIf("@useHpSlider")] [DisableIf("@true")] [SerializeField] private Slider hpSlider; // Data [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] protected bool beAttacked; [field: Title("드롭 아이템")] [field: SerializeField] public int ItemDropTableIdx { get; set; } // 일반 변수 protected bool isAttacking; protected bool usedNormalAttackCoroutine; protected WaitForSeconds waitAtkCooldown; // 컴포넌트 protected Rigidbody rb; public Collider MyCollider { get; set; } public NavMeshAgent Agent { get; set; } protected BehaviorTree bt; private Transform unitRoot; protected Animator myAnimator; private Canvas worldSpaceCanvas; // Hash protected static readonly int RunStateHash = Animator.StringToHash("RunState"); protected static readonly int DieHash = Animator.StringToHash("Die"); // 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 && IsRandomMove) { 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(); unitRoot = transform.Find("UnitRoot"); if (unitRoot == null) { print("UnitRoot를 찾을 수 없습니다."); } else { myAnimator = unitRoot.GetComponent(); if (myAnimator == null) { print("myAnimator를 찾을 수 없습니다."); } } worldSpaceCanvas = GameObject.Find("WorldSpaceCanvas")?.GetComponent(); if (worldSpaceCanvas == null) { print("WorldSpaceCanvas 찾을 수 없습니다."); } else { if (useHpSlider) { hpSlider = Instantiate(hpSliderPrefab, worldSpaceCanvas.transform).GetComponent(); hpSlider.gameObject.name = gameObject.name + " HpSlider"; if (unitRoot) { hpSlider.transform.rotation = unitRoot.transform.rotation; } } } } protected override void Start() { base.Start(); HelpLayer = LayerMask.GetMask("Enemy"); TargetLayer = LayerMask.GetMask("Player") | LayerMask.GetMask("Crewmate"); waitAtkCooldown = new WaitForSeconds(AtkCooldown); Agent.updateRotation = false; DefensePos = transform.position; SetAgentSpeed(MoveSpd); hpSlider.maxValue = MaxHp; SetCurrentHp(MaxHp); } protected override void Update() { HpSliderUpdate(); if (CurrentHp <= 0) return; MoveUpdate(); FlipCharacterUpdate(); } #endregion #region Interface // IDamageable public virtual void TakeDamage(float attackerPower, 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) { Die(); return; } StartCoroutine(nameof(BeAttacked)); } [Button("Die테스트")] public void Die() { myAnimator?.SetTrigger(DieHash); MyCollider.enabled = false; Agent.isStopped = true; Agent.enabled = false; Invoke(nameof(DieEvent), 2f); } private void DieEvent() { ItemManager.Inst.ItemDropRandomPosition(ItemDropTableIdx, transform.position + Vector3.up * 0.5f); Destroy(hpSlider.gameObject); Destroy(gameObject); } public float GetCurrentHp() => CurrentHp; // 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(Vector3 centerPos, bool targetIsTrigger = true) { Array.Clear(Targets, 0, MAX_COLLIDERS); var numResults = Physics.OverlapSphereNonAlloc(centerPos, 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 = (centerPos - Targets[i].bounds.center).sqrMagnitude; if (distanceSqrToTarget >= nearestDistance) continue; nearestDistance = distanceSqrToTarget; nearestTargetCollider = Targets[i]; } SetTarget(nearestTargetCollider); } public void SetTarget(Collider value) { Target = value; if (value != null) { IsCombated = true; BeAttackedInIdle = false; } } public virtual bool IsTargetWithinRange(Vector3 centerPos, float range) { var inRange = Vector3.Distance(centerPos, Target.bounds.center) <= range; return inRange; } 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, float speed, float stopDistance = float.MaxValue) { 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(speed); Agent.stoppingDistance = stopDistance; Agent.isStopped = false; Agent.SetDestination(targetPos); } // 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 void HpSliderUpdate() { switch (useHpSlider) { case true when CurrentHp > 0 && CurrentHp < MaxHp: { if (!hpSlider.gameObject.activeSelf) { hpSlider.gameObject.SetActive(true); } var localOffset = unitRoot.TransformPoint(hpSliderOffset); hpSlider.transform.position = localOffset; break; } case true when CurrentHp <= 0 || CurrentHp >= MaxHp: { if (hpSlider.gameObject.activeSelf) { hpSlider.gameObject.SetActive(false); } break; } } } private void MoveUpdate() { float runStateValue; // 움직이는 경우 if (Agent.velocity.x != 0 || Agent.velocity.z != 0) { runStateValue = 0.5f; } // 멈춰있는 경우 else { runStateValue = 0f; } if (!beAttacked) { myAnimator?.SetFloat(RunStateHash, runStateValue); } } private void FlipCharacterUpdate() { var localScale = transform.localScale; if (Agent.velocity.x != 0) { localScale.x = Agent.velocity.x > 0 ? Mathf.Abs(localScale.x) : -Mathf.Abs(localScale.x); } else { if (Target) { var targetToDistanceX = Target.bounds.center.x - MyCollider.bounds.center.x; localScale.x = targetToDistanceX > 0 ? Mathf.Abs(localScale.x) : -Mathf.Abs(localScale.x); } } transform.localScale = localScale; } private IEnumerator BeAttacked() { beAttacked = true; myAnimator?.SetFloat(RunStateHash, 1f); yield return BeAttackedWaitTime; beAttacked = false; } 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 SetCurrentHp(float value) { CurrentHp = value; if (useHpSlider) { hpSlider.value = value; } } private void SetAgentSpeed(float value) => Agent.speed = value; private void SetAtkWaitCooldown() => waitAtkCooldown = new WaitForSeconds(AtkCooldown); #endregion } }