using System; using System.Collections; using System.Collections.Generic; using Sirenix.OdinInspector; using UnityEngine; using UnityEngine.AI; using Random = UnityEngine.Random; // ReSharper disable once CheckNamespace namespace BlueWaterProject { public enum AttackerType { NONE, PLAYER, PIRATE, ENEMY } [Serializable] public abstract class AiController : MonoBehaviour, IDamageable, IFieldOfView { #region Property and variable protected bool isAttacking; protected AttackerType attackerType; [SerializeField] protected List skinMaterial = new(10); protected Animator aiAnimator; protected AiMover aiMover; protected NavMeshAgent navMeshAgent; private UnitController mouseEnterUnitController; private UnitSelection unitSelection; public static readonly int SpeedHash = Animator.StringToHash("Speed"); public static readonly int AttackHash = Animator.StringToHash("Attack"); public static readonly int DamageHash = Animator.StringToHash("TakeDamage"); public static readonly int DeathTypeHash = Animator.StringToHash("DeathType"); public static readonly int DeathHash = Animator.StringToHash("Death"); public static readonly int OutlineColorHash = Shader.PropertyToID("_OutlineColor"); private static readonly WaitForSeconds FindTargetWaitTime = new(0.5f); #endregion #region abstract function protected abstract void Attack(); #endregion #region Unity built-in function private void OnDrawGizmosSelected() { DrawGizmosInFieldOfView(); } protected virtual void Awake() { FindMaterial(); aiAnimator = Utils.GetComponentAndAssert(transform); aiMover = Utils.GetComponentAndAssert(transform); navMeshAgent = Utils.GetComponentAndAssert(transform); unitSelection = FindObjectOfType(); if (gameObject.layer == LayerMask.NameToLayer("Player")) { attackerType = AttackerType.PLAYER; } else if (gameObject.layer == LayerMask.NameToLayer("Pirate")) { attackerType = AttackerType.PIRATE; } else if (gameObject.layer == LayerMask.NameToLayer("Enemy")) { attackerType = AttackerType.ENEMY; } } private void Start() { SetCurrentHp(AiStat.maxHp); navMeshAgent.speed = AiStat.moveSpd; StartCoroutine(nameof(FindTarget)); Attack(); } private void FixedUpdate() { UpdateLookAtTarget(); } private void OnMouseEnter() { mouseEnterUnitController = gameObject.GetComponentInParent(); if (mouseEnterUnitController == unitSelection.SelectedUnitController) return; foreach (var soldier in mouseEnterUnitController.unit.soldierList) { soldier.MouseEnterHighlight(); } } private void OnMouseExit() { if (!mouseEnterUnitController || mouseEnterUnitController == unitSelection.SelectedUnitController) return; foreach (var soldier in mouseEnterUnitController.unit.soldierList) { soldier.ResetHighlight(); } mouseEnterUnitController = null; } #endregion #region interface property and function #region IAiStat [field: Space(10f)] [field: Title("AiStat")] [field: SerializeField] public AiStat AiStat { get; set; } = new(); public float GetCurrentHp() => AiStat.currentHp; public void TakeDamage(AiStat attacker, AiStat defender) { // 회피 성공 체크 if (Random.Range(0, 100) < defender.avoidanceRate) { // TODO : 회피 처리 return; } var finalDamage = Utils.CalcDamage(attacker, defender); // 방패 막기 체크 if (finalDamage == 0f) { // TODO : 방패로 막힘 처리(애니메이션 등) return; } var changeHp = Mathf.Max(defender.currentHp - finalDamage, 0); SetCurrentHp(changeHp); // 죽었는지 체크 if (changeHp == 0f) { var randomValue = Random.Range(0, 2); aiAnimator.SetInteger(DeathTypeHash, randomValue); // TODO : 죽었을 때 처리(죽는 애니메이션 이후 사라지는 효과 등) aiAnimator.SetTrigger(DeathHash); Invoke(nameof(DestroyObject), 5f); return; } aiAnimator.SetTrigger(DamageHash); } #endregion #region IFieldOfView [field: Space(10f)] [field: Title("FieldOfView")] [field: SerializeField] public bool IsDrawGizmosInFieldOfView { get; set; } = true; [field: SerializeField] public LayerMask TargetLayer { get; set; } [field: SerializeField] public float ViewRadius { get; set; } [field: SerializeField] public Collider[] ColliderWithinRange { get; set; } = new Collider[TARGET_MAX_SIZE]; [field: SerializeField] public List TargetInfoList { get; set; } = new(TARGET_MAX_SIZE); [field: SerializeField] public IAiStat IaiStat { get; set; } [field: SerializeField] public TargetInfo TargetInfo { get; set; } = new(); private const int TARGET_MAX_SIZE = 30; #if UNITY_EDITOR public void DrawGizmosInFieldOfView() { if (!IsDrawGizmosInFieldOfView) return; var myPos = transform.position; Gizmos.color = Color.green; Gizmos.DrawWireSphere(myPos, ViewRadius); if (TargetInfo.transform == null) return; Debug.DrawLine(myPos, TargetInfo.transform.position, Color.red); } #endif public IEnumerator FindTarget() { while (true) { Array.Clear(ColliderWithinRange, 0, TARGET_MAX_SIZE); TargetInfoList.Clear(); var myPos = transform.position; var maxColliderCount = Physics.OverlapSphereNonAlloc(myPos, ViewRadius, ColliderWithinRange, TargetLayer, QueryTriggerInteraction.Collide); if (maxColliderCount <= 0) { TargetInfo.DefaultSetting(); yield return FindTargetWaitTime; continue; } for (var i = 0; i < maxColliderCount; i++) { IaiStat = ColliderWithinRange[i].GetComponent(); if (IaiStat != null) { TargetInfoList.Add(new TargetInfo(ColliderWithinRange[i].transform, ColliderWithinRange[i], IaiStat)); } } if (TargetInfoList.Count <= 0) { TargetInfo.DefaultSetting(); yield return FindTargetWaitTime; continue; } int nearestIndex; float nearestDistance; if (TargetInfoList[0].iAiStat.GetCurrentHp() <= 0) { nearestIndex = 0; nearestDistance = float.PositiveInfinity; } else { nearestIndex = 0; nearestDistance = Vector3.Distance(TargetInfoList[0].transform.position, myPos); } for (var i = 1; i < TargetInfoList.Count; i++) { var distance = Vector3.Distance(TargetInfoList[i].transform.position, myPos); if (nearestDistance < distance || TargetInfoList[i].iAiStat.GetCurrentHp() <= 0) continue; nearestIndex = i; nearestDistance = distance; } if (TargetInfoList[nearestIndex].transform) { TargetInfo.SetTargetInfo(TargetInfoList[nearestIndex].transform, TargetInfoList[nearestIndex].collider, TargetInfoList[nearestIndex].iAiStat); } else { TargetInfo.DefaultSetting(); } yield return FindTargetWaitTime; } } public void UpdateLookAtTarget() { if (TargetInfo.transform) { navMeshAgent.updateRotation = false; var targetPos = TargetInfo.transform.position; targetPos.y = transform.position.y; transform.LookAt(targetPos); } else { navMeshAgent.updateRotation = true; } } #endregion #endregion #region Custom function private void FindMaterial() { var skinnedMeshRenderers = GetComponentsInChildren(); var meshRenderers = GetComponentsInChildren(); foreach (var skin in skinnedMeshRenderers) { if (!skin.gameObject.activeSelf) continue; skinMaterial.Add(skin.material); } foreach (var skin in meshRenderers) { if (!skin.gameObject.activeSelf) continue; skinMaterial.Add(skin.material); } } public void ResetHighlight() { foreach (var skin in skinMaterial) { skin.SetColor(OutlineColorHash, Color.black); } } public void MouseEnterHighlight() { foreach (var skin in skinMaterial) { skin.SetColor(OutlineColorHash, Color.white); } } public void SelectedHighlight() { foreach (var skin in skinMaterial) { skin.SetColor(OutlineColorHash, Color.blue); } } private void DestroyObject() => Destroy(gameObject); public bool GetIsAttacking() => isAttacking; public NavMeshAgent GetNavMeshAgent() => navMeshAgent; public Animator GetAnimator() => aiAnimator; public void SetCurrentHp(float value) => AiStat.currentHp = value; #endregion } }