using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Sirenix.OdinInspector; using Unity.VisualScripting; using UnityEngine; using UnityEngine.AI; using Random = UnityEngine.Random; // ReSharper disable once CheckNamespace namespace BlueWaterProject { public enum AiType { NONE = -1, PLAYER, PIRATE, ENEMY } public enum AttackerType { NONE = -1, OFFENSE, DEFENSE } public enum OffenseType { NONE = -1, NORMAL, ONLY_HOUSE } public enum DefenseType { NONE = -1, NORMAL } [Serializable] public class AiController : MonoBehaviour, IDamageable, IFieldOfView, IAiMover { #region Property and variable [Title("AiType")] [EnableIf("alwaysFalse")] [EnumToggleButtons] [SerializeField] protected AttackerType attackerType; private bool alwaysFalse; [EnableIf("alwaysFalse")] [ShowIf("attackerType", AttackerType.OFFENSE)] [SerializeField] private OffenseType offenseType; [EnableIf("alwaysFalse")] [ShowIf("attackerType", AttackerType.DEFENSE)] [SerializeField] private DefenseType defenseType; [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; protected bool isAttacking; private Vector3 commandedPos; protected Transform backpackContainer; protected Transform leftWeaponContainer; protected Transform leftShieldContainer; protected Transform headContainer; protected Transform rightWeaponContainer; protected Transform bodyContainer; protected Transform flagContainer; public IslandInfo IslandInfo { get; set; } protected Animator aiAnimator; protected NavMeshAgent navMeshAgent; private UnitController myUnitController; private UnitController mouseEnterUnitController; private UnitSelection unitSelection; private CapsuleCollider myCollider; private CapsuleCollider hitBoxCollider; protected CloseWeapon closeWeapon; 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"); protected static readonly WaitForSeconds FindTargetWaitTime = new(0.5f); #endregion #region Unity built-in function #if UNITY_EDITOR protected virtual void OnDrawGizmosSelected() { DrawGizmosInFieldOfView(); } #endif protected virtual void Awake() { FindMaterial(); backpackContainer = Utils.GetComponentAndAssert(transform. Find("Bip001/Bip001 Pelvis/Bip001 Spine/Backpack_container")); leftWeaponContainer = Utils.GetComponentAndAssert(transform. Find("Bip001/Bip001 Pelvis/Bip001 Spine/Bip001 L Clavicle/Bip001 L UpperArm/Bip001 L Forearm/Bip001 L Hand/L_hand_container")); leftShieldContainer = Utils.GetComponentAndAssert(transform. Find("Bip001/Bip001 Pelvis/Bip001 Spine/Bip001 L Clavicle/Bip001 L UpperArm/Bip001 L Forearm/Bip001 L Hand/L_shield_container")); headContainer = Utils.GetComponentAndAssert(transform. Find("Bip001/Bip001 Pelvis/Bip001 Spine/Bip001 Neck/Bip001 Head/Head_container")); rightWeaponContainer = Utils.GetComponentAndAssert(transform. Find("Bip001/Bip001 Pelvis/Bip001 Spine/Bip001 R Clavicle/Bip001 R UpperArm/Bip001 R Forearm/Bip001 R Hand/R_hand_container")); bodyContainer = Utils.GetComponentAndAssert(transform. Find("Body_container")); flagContainer = Utils.GetComponentAndAssert(transform. Find("Flag_container")); 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(); } private void Start() { switch (AiStat.AiType) { case AiType.NONE: break; case AiType.PLAYER: gameObject.layer = LayerMask.NameToLayer("Player"); hitBoxCollider.gameObject.layer = LayerMask.NameToLayer("Player"); break; case AiType.PIRATE: gameObject.layer = LayerMask.NameToLayer("Pirate"); hitBoxCollider.gameObject.layer = LayerMask.NameToLayer("Pirate"); break; case AiType.ENEMY: gameObject.layer = LayerMask.NameToLayer("Enemy"); hitBoxCollider.gameObject.layer = LayerMask.NameToLayer("Enemy"); break; default: throw new ArgumentOutOfRangeException(); } InitViewModel(); SetCurrentHp(AiStat.MaxHp); SetMoveSpeed(AiStat.MoveSpd); switch (attackerType) { case AttackerType.NONE: break; case AttackerType.OFFENSE: StartCoroutine(nameof(FindTargetInOffense)); break; case AttackerType.DEFENSE: StartCoroutine(nameof(FindTarget)); break; default: throw new ArgumentOutOfRangeException(); } Attack(); } private void OnDisable() { RemoveIslandInfo(); StopAllCoroutines(); } private void Update() { aiAnimator.SetFloat(SpeedHash, navMeshAgent.velocity.normalized.magnitude); } private void FixedUpdate() { UpdateLookAtTarget(); UpdateMovement(); } private void OnMouseEnter() { mouseEnterUnitController = gameObject.GetComponentInParent(); if (mouseEnterUnitController == unitSelection.SelectedUnitController) return; foreach (var soldier in mouseEnterUnitController.unit.UnitList) { soldier.MouseEnterHighlight(); } } private void OnMouseExit() { if (!mouseEnterUnitController || mouseEnterUnitController == unitSelection.SelectedUnitController) return; foreach (var soldier in mouseEnterUnitController.unit.UnitList) { 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 SetCurrentHp(float value) => AiStat.CurrentHp = value; 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) { RemoveIslandInfo(); StopAllCoroutines(); navMeshAgent.enabled = false; myCollider.enabled = false; hitBoxCollider.enabled = false; 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 IAiStat IaiStat { get; set; } [field: SerializeField] public Transform TargetTransform { get; set; } 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 IEnumerator FindTargetInOffense() { while (true) { if (CanAttack()) { yield return FindTargetWaitTime; continue; } switch (offenseType) { case OffenseType.NONE: break; case OffenseType.NORMAL: if (IslandInfo.EnemyList.Count > 0) { SetNearestTargetDestination(IslandInfo.EnemyList); } else if (IslandInfo.HouseList.Count > 0) { SetNearestTargetDestination(IslandInfo.HouseList); } break; case OffenseType.ONLY_HOUSE: if (navMeshAgent.pathStatus == NavMeshPathStatus.PathPartial) { SetNearestTargetDestination(IslandInfo.TargetAllList); } else { if (IslandInfo.HouseList.Count > 0) { SetNearestTargetDestination(IslandInfo.HouseList); } else if (IslandInfo.EnemyList.Count > 0) { SetNearestTargetDestination(IslandInfo.EnemyList); } } break; default: throw new ArgumentOutOfRangeException(); } yield return FindTargetWaitTime; } } public void SetNearestTargetDestination(List targetList) { if (targetList.Count <= 0) return; var nearestTarget = targetList.OrderBy(t => { var targetTransform = (Transform)(object)t; var targetCollider = targetTransform.GetComponent(); if (!targetCollider) { return float.MaxValue; } var closestPoint = targetCollider.ClosestPoint(transform.position); return Vector3.Distance(transform.position, closestPoint); }) .FirstOrDefault(); if (nearestTarget == null) return; TargetTransform = (Transform)(object)nearestTarget; navMeshAgent.SetDestination(TargetTransform.position); } public virtual void UpdateLookAtTarget() { if (CanAttack()) { 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() { // if (IsCommanded) // { // if (navMeshAgent.destination == commandedPos) // { // if (navMeshAgent.remainingDistance <= navMeshAgent.stoppingDistance) // { // IsCommanded = false; // } // } // else // { // if (isAttacking) return; // // navMeshAgent.SetDestination(commandedPos); // } // } } 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 public void InitViewModel() { SetActiveViewModel(backpackContainer, DataManager.Inst.GetAiViewDictionaryKey(AiStat.ViewIdx).Backpack); SetActiveViewModel(leftWeaponContainer, DataManager.Inst.GetAiViewDictionaryKey(AiStat.ViewIdx).LeftWeapon); SetActiveViewModel(leftShieldContainer, DataManager.Inst.GetAiViewDictionaryKey(AiStat.ViewIdx).LeftShield); SetActiveViewModel(headContainer, DataManager.Inst.GetAiViewDictionaryKey(AiStat.ViewIdx).Head); SetActiveViewModel(rightWeaponContainer, DataManager.Inst.GetAiViewDictionaryKey(AiStat.ViewIdx).RightWeapon); SetActiveViewModel(bodyContainer, DataManager.Inst.GetAiViewDictionaryKey(AiStat.ViewIdx).Body); SetActiveViewModel(flagContainer, DataManager.Inst.GetAiViewDictionaryKey(AiStat.ViewIdx).Flag); if (DataManager.Inst.GetAiViewDictionaryKey(AiStat.ViewIdx).RightWeapon == -1) return; closeWeapon = rightWeaponContainer.GetChild(DataManager.Inst.GetAiViewDictionaryKey(AiStat.ViewIdx).RightWeapon).AddComponent(); closeWeapon.gameObject.layer = LayerMask.NameToLayer("Weapon"); closeWeapon.SetAttackerType(attackerType); } private void SetActiveViewModel(Transform container, int model) { foreach (Transform item in container) { if (!item.gameObject.activeSelf) continue; item.gameObject.SetActive(false); } if (model != -1) { container.GetChild(model).gameObject.SetActive(true); } } protected virtual void Attack() { StartCoroutine(nameof(AttackAnimation)); } private IEnumerator AttackAnimation() { while (true) { if (!CanAttack()) { isAttacking = false; yield return FindTargetWaitTime; continue; } isAttacking = true; closeWeapon.SetIsAttacked(false); closeWeapon.SetAttackerStat(AiStat); aiAnimator.SetTrigger(AttackHash); while (isAttacking) { yield return null; } yield return new WaitForSeconds(AiStat.AtkCooldown); } } protected virtual bool CanAttack() { if (!TargetTransform || !IslandInfo.TargetAllList.Contains(TargetTransform)) return false; var targetInAttackRange = Vector3.Distance(transform.position, TargetTransform.position) <= AiStat.AtkRange; if (targetInAttackRange) { SetAgentIsStopped(true); return true; } SetAgentIsStopped(false); return false; } 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); } } private void RemoveIslandInfo() { if (!IslandInfo) return; IslandInfo.RemoveListElement(IslandInfo.EnemyList, transform); } private void SetAgentIsStopped(bool value) { if (navMeshAgent.enabled) { navMeshAgent.isStopped = value; } } public void SetAttackerType(AttackerType type) => attackerType = type; public void SetOffenseType(OffenseType type) => offenseType = type; public void SetDefenseType(DefenseType type) => defenseType = type; 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 SetMoveSpeed(float value) => navMeshAgent.speed = value; #endregion } }