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, IAiMover { #region Property and variable [Title("Skin")] [Tooltip("SkinnedMeshRenderer, MeshRenderer의 Material을 모두 담고 있는 리스트")] [SerializeField] protected List skinMaterialList = new(10); [Tooltip("캐릭터 외곽선의 기본 색상")] [SerializeField] protected Color defaultSkinColor = Color.black; [Tooltip("캐릭터에 마우스 커서가 올라가 있을 때 색상")] [SerializeField] protected Color mouseEnterHighlightSkinColor = Color.white; [Tooltip("캐릭터가 선택되었을 때 색상")] [SerializeField] protected Color selectedSkinColor = Color.blue; [SerializeField] protected bool isAttacking; [SerializeField] protected AttackerType attackerType; private Vector3 commandedPos; protected Animator aiAnimator; protected NavMeshAgent navMeshAgent; private UnitController myUnitController; private UnitController mouseEnterUnitController; private UnitSelection unitSelection; private CapsuleCollider myCollider; private CapsuleCollider hitBoxCollider; private static readonly int SpeedHash = Animator.StringToHash("Speed"); protected static readonly int AttackHash = Animator.StringToHash("Attack"); private static readonly int DamageHash = Animator.StringToHash("TakeDamage"); private static readonly int DeathTypeHash = Animator.StringToHash("DeathType"); private static readonly int DeathHash = Animator.StringToHash("Death"); private static readonly int ShieldHash = Animator.StringToHash("Shield"); private 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 #if UNITY_EDITOR private void OnDrawGizmosSelected() { DrawGizmosInFieldOfView(); } #endif protected virtual void Awake() { FindMaterial(); aiAnimator = Utils.GetComponentAndAssert(transform); navMeshAgent = Utils.GetComponentAndAssert(transform); myUnitController = Utils.GetComponentAndAssert(transform.parent); myCollider = Utils.GetComponentAndAssert(transform); hitBoxCollider = Utils.GetComponentAndAssert(transform.Find("HitBox")); 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(); UpdateMovement(); } 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, Vector3? attackPos = null) { if (!TargetTransform && attackPos != null) { BeAttackedMovement((Vector3)attackPos); } // 회피 성공 체크 if (Random.Range(0, 100) < defender.avoidanceRate) { // TODO : 회피 처리 return; } var finalDamage = Utils.CalcDamage(attacker, defender); // 방패 막기 체크 if (finalDamage == 0f) { aiAnimator.SetTrigger(ShieldHash); return; } var changeHp = Mathf.Max(defender.currentHp - finalDamage, 0); SetCurrentHp(changeHp); // 죽었는지 체크 if (changeHp == 0f) { myCollider.enabled = false; hitBoxCollider.enabled = false; navMeshAgent.isStopped = true; navMeshAgent.velocity = Vector3.zero; var randomValue = Random.Range(0, 2); aiAnimator.SetInteger(DeathTypeHash, randomValue); // TODO : 죽었을 때 처리(죽는 애니메이션 이후 사라지는 효과 등) aiAnimator.SetTrigger(DeathHash); Invoke(nameof(DestroyObject), 3f); 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 Transform TargetTransform { get; set; } //[field: SerializeField] public TargetInfo TargetInfo { get; set; } = new(); private const int TARGET_MAX_SIZE = 30; public void DrawGizmosInFieldOfView() { if (!IsDrawGizmosInFieldOfView) return; var myPos = transform.position; Gizmos.color = Color.green; Gizmos.DrawWireSphere(myPos, ViewRadius); if (!TargetTransform) return; Debug.DrawLine(myPos, TargetTransform.position, Color.red); } public IEnumerator FindTarget() { while (true) { Array.Clear(ColliderWithinRange, 0, TARGET_MAX_SIZE); var myPos = transform.position; var maxColliderCount = Physics.OverlapSphereNonAlloc(myPos, ViewRadius, ColliderWithinRange, TargetLayer, QueryTriggerInteraction.Collide); if (maxColliderCount <= 0) { TargetTransform = null; yield return FindTargetWaitTime; continue; } var nearestDistance = Mathf.Infinity; Transform nearestTargetTransform = null; for (var i = 0; i < maxColliderCount; i++) { var distanceToTarget = Vector3.Distance(transform.position, ColliderWithinRange[i].transform.position); if (distanceToTarget >= nearestDistance) continue; nearestDistance = distanceToTarget; nearestTargetTransform = ColliderWithinRange[i].transform; } TargetTransform = nearestTargetTransform; yield return FindTargetWaitTime; } } public void UpdateLookAtTarget() { if (TargetTransform) { navMeshAgent.updateRotation = false; var targetPos = TargetTransform.position; targetPos.y = transform.position.y; transform.LookAt(targetPos); } else { navMeshAgent.updateRotation = true; } } #endregion #region IAiMover [field: Space(10f)] [field: Title("AiMover")] [field: SerializeField] public MoveType AttackMoveType { get; set; } [field: SerializeField] public MoveType BeAttackedMoveType { get; set; } [field: SerializeField] public bool IsCommanded { get; set; } public void UpdateMovement() { aiAnimator.SetFloat(SpeedHash, navMeshAgent.velocity.normalized.magnitude); if (IsCommanded) { if (navMeshAgent.destination == commandedPos) { if (navMeshAgent.remainingDistance <= navMeshAgent.stoppingDistance) { IsCommanded = false; } } else { if (isAttacking) return; navMeshAgent.SetDestination(commandedPos); } } else { if (!TargetTransform) return; switch (AttackMoveType) { case MoveType.NONE: break; case MoveType.FIXED: break; case MoveType.MOVE: if (Vector3.Distance(transform.position, TargetTransform.position) > AiStat.atkRange) { navMeshAgent.SetDestination(TargetTransform.position); } break; default: throw new ArgumentOutOfRangeException(); } } } public void BeAttackedMovement(Vector3 attackPos) { if (TargetTransform) return; switch (BeAttackedMoveType) { case MoveType.NONE: break; case MoveType.FIXED: break; case MoveType.MOVE: if (Vector3.Distance(transform.position, attackPos) > AiStat.atkRange) { myUnitController.MoveCommand(attackPos); } break; default: throw new ArgumentOutOfRangeException(); } } public void MoveTarget(Vector3 targetPos) { IsCommanded = true; commandedPos = targetPos; } #endregion #endregion #region Custom function private void FindMaterial() { var skinnedMeshRenderers = GetComponentsInChildren(); var meshRenderers = GetComponentsInChildren(); foreach (var skin in skinnedMeshRenderers) { if (!skin.gameObject.activeSelf) continue; skinMaterialList.Add(skin.material); } foreach (var skin in meshRenderers) { if (!skin.gameObject.activeSelf) continue; skinMaterialList.Add(skin.material); } } private void SetOutlineColor(Color color) { foreach (var skin in skinMaterialList) { skin.SetColor(OutlineColorHash, color); } } public void ResetHighlight() => SetOutlineColor(defaultSkinColor); public void MouseEnterHighlight() => SetOutlineColor(mouseEnterHighlightSkinColor); public void SelectedHighlight() => SetOutlineColor(selectedSkinColor); private void DestroyObject() => Destroy(gameObject); public void OnAttacking(int boolValue) => isAttacking = boolValue == 1; public NavMeshAgent GetNavMeshAgent() => navMeshAgent; public Animator GetAnimator() => aiAnimator; public void SetCurrentHp(float value) => AiStat.currentHp = value; #endregion } }