using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Sirenix.OdinInspector; using Unity.VisualScripting; using UnityEngine; using UnityEngine.AI; using UnityEngine.Animations; using Random = UnityEngine.Random; // ReSharper disable once CheckNamespace namespace BlueWaterProject { public enum EAiType { NONE = -1, PLAYER, PIRATE, ENEMY } public enum EAttackerType { NONE = -1, OFFENSE, DEFENSE } public enum EOffenseType { NONE = -1, NORMAL, ONLY_HOUSE } public enum EDefenseType { NONE = -1, STRIKER, MIDFIELDER, DEFENDER, KEEPER } [Serializable] public class AiController : MonoBehaviour, IDamageable { #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.red; [DisableIf("@true")] [SerializeField] private IslandInfo islandInfo; public bool IsAttackCoroutine { get; set; } public Vector3 DefensePos { get; set; } protected bool isAttacking; protected Transform backpackContainer; protected Transform leftWeaponContainer; protected Transform leftShieldContainer; protected Transform headContainer; protected Transform rightWeaponContainer; protected Transform bodyContainer; protected Transform flagContainer; protected Animator aiAnimator; protected NavMeshAgent navMeshAgent; //private UnitController myUnitController; //private UnitController mouseEnterUnitController; private UnitSelection unitSelection; private CapsuleCollider myCollider; private LookAtConstraint lookAtConstraint; protected 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); private static readonly WaitForSeconds CheckAgentArriveTime = new(0.1f); private delegate EnemyView GetAiViewDataDelegate(string viewIdx); #endregion #region Unity built-in function #if UNITY_EDITOR protected virtual void OnDrawGizmosSelected() { DrawGizmosInFieldOfView(); } #endif protected virtual void Awake() { InitComponent(); } private void Start() { InitStart(); ExecuteFindTarget(); //Attack(); navMeshAgent.stoppingDistance = AiStat.AtkRange; } private void OnDisable() { RemoveIslandInfo(); StopAllCoroutines(); } private void Update() { aiAnimator.SetFloat(SpeedHash, navMeshAgent.velocity.normalized.magnitude); // if (!TargetTransform || !navMeshAgent.enabled) return; // // var distanceToTarget = Vector3.Distance(transform.position, TargetTransform.position); // // navMeshAgent.isStopped = distanceToTarget <= navMeshAgent.stoppingDistance; // switch (AiStat.AttackerType) // { // case EAttackerType.NONE: // break; // case EAttackerType.OFFENSE: // navMeshAgent.SetDestination(distanceToTarget <= AiStat.AtkRange ? transform.position : TargetTransform.position); // break; // case EAttackerType.DEFENSE: // switch (AiStat.DefenseType) // { // case EDefenseType.NONE: // print("AiStat.DefenseType == NONE Error"); // break; // case EDefenseType.STRIKER: // navMeshAgent.SetDestination(distanceToTarget <= AiStat.AtkRange ? transform.position : TargetTransform.position); // break; // case EDefenseType.MIDFIELDER: // navMeshAgent.SetDestination(distanceToTarget <= AiStat.AtkRange ? transform.position : TargetTransform.position); // break; // case EDefenseType.DEFENDER: // navMeshAgent.SetDestination(distanceToTarget <= AiStat.AtkRange ? transform.position : TargetTransform.position); // break; // case EDefenseType.KEEPER: // break; // default: // throw new ArgumentOutOfRangeException(); // } // break; // default: // throw new ArgumentOutOfRangeException(); // } } private void FixedUpdate() { UpdateLookAtTarget(); } // private void OnMouseEnter() // { // if (AiStat.AiType == EAiType.ENEMY) return; // // mouseEnterUnitController = gameObject.GetComponentInParent(); // // if (mouseEnterUnitController == unitSelection.SelectedUnitController) return; // // foreach (var pirateUnitStat in mouseEnterUnitController.pirateUnitStat.UnitList) // { // pirateUnitStat.MouseEnterHighlight(); // } // } // // private void OnMouseExit() // { // if (AiStat.AiType == EAiType.ENEMY || // !mouseEnterUnitController || mouseEnterUnitController == unitSelection.SelectedUnitController) return; // // foreach (var pirateUnitStat in mouseEnterUnitController.pirateUnitStat.UnitList) // { // pirateUnitStat.ResetHighlight(); // } // // mouseEnterUnitController = null; // } #endregion #region interface property and function #region IAiStat [field: Space(10f)] [field: Title("AiStat")] [field: SerializeField] public EnemyStat AiStat { get; set; } = new(); public float GetCurrentHp() => AiStat.CurrentHp; public void SetCurrentHp(float value) => AiStat.CurrentHp = value; public void TakeDamage(float attackerPower, float attackerShieldPenetrationRate, Vector3? attackPos = null) { if (attackPos != null && navMeshAgent.enabled && AiStat.AttackerType == EAttackerType.DEFENSE && !TargetTransform) { BeAttackedMovement((Vector3)attackPos); } // 회피 성공 체크 if (Random.Range(0, 100) < AiStat.AvoidanceRate) { // TODO : 회피 처리 return; } var finalDamage = Utils.CalcDamage(attackerPower, attackerShieldPenetrationRate, AiStat); // 방패 막기 체크 if (finalDamage == 0f) { aiAnimator.SetTrigger(ShieldHash); return; } var changeHp = Mathf.Max(AiStat.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 Collider[] ColliderWithinRange { get; set; } = new Collider[TARGET_MAX_SIZE]; [field: SerializeField] public Transform TargetTransform { get; set; } protected const int TARGET_MAX_SIZE = 30; public void DrawGizmosInFieldOfView() { if (!IsDrawGizmosInFieldOfView) return; var myPos = transform.position; Gizmos.color = Color.green; Gizmos.DrawWireSphere(myPos, AiStat.ViewRange); Gizmos.color = Color.blue; Gizmos.DrawWireSphere(myPos, AiStat.DefenseRange); if (!TargetTransform) return; Debug.DrawLine(myPos, TargetTransform.position, Color.red); } public IEnumerator FindTargetInOffense() { while (true) { switch (AiStat.OffenseType) { case EOffenseType.NONE: print("AiStat.OffenseType == NONE Error"); break; case EOffenseType.NORMAL: if (islandInfo.EnemyList.Count > 0) { SetNearestTargetInOffense(islandInfo.EnemyList); } else if (islandInfo.HouseList.Count > 0) { SetNearestTargetInOffense(islandInfo.HouseList); } break; case EOffenseType.ONLY_HOUSE: if (navMeshAgent.pathStatus == NavMeshPathStatus.PathPartial) { SetNearestTargetInOffense(islandInfo.TargetAllList); } else { if (islandInfo.HouseList.Count > 0) { SetNearestTargetInOffense(islandInfo.HouseList); } else if (islandInfo.EnemyList.Count > 0) { SetNearestTargetInOffense(islandInfo.EnemyList); } } break; default: throw new ArgumentOutOfRangeException(); } if (TargetTransform && navMeshAgent.enabled) { var distanceToTarget = Vector3.Distance(transform.position, TargetTransform.position); navMeshAgent.isStopped = distanceToTarget <= navMeshAgent.stoppingDistance; if (distanceToTarget > navMeshAgent.stoppingDistance) { navMeshAgent.SetDestination(TargetTransform.position); } } yield return FindTargetWaitTime; } } public IEnumerator FindTargetInDefense() { while (true) { switch (AiStat.DefenseType) { case EDefenseType.NONE: print("AiStat.DefenseType == NONE Error"); break; case EDefenseType.STRIKER: SetNearestTargetInDefense(transform.position, AiStat.ViewRange); break; case EDefenseType.MIDFIELDER: SetNearestTargetInDefense(transform.position, AiStat.ViewRange); break; case EDefenseType.DEFENDER: SetNearestTargetInDefense(DefensePos, AiStat.DefenseRange); break; case EDefenseType.KEEPER: SetNearestTargetInDefense(transform.position, AiStat.ViewRange); break; default: throw new ArgumentOutOfRangeException(); } if (TargetTransform && navMeshAgent.enabled) { var distanceToTarget = Vector3.Distance(transform.position, TargetTransform.position); navMeshAgent.isStopped = distanceToTarget <= navMeshAgent.stoppingDistance; if (distanceToTarget > navMeshAgent.stoppingDistance) { navMeshAgent.SetDestination(TargetTransform.position); } } yield return FindTargetWaitTime; } } public virtual void SetNearestTargetInOffense(List targetList) { if (targetList.Count <= 0) return; var nearestTarget = targetList.OrderBy(t => { var myPos = transform.position; var targetTransform = (Transform)(object)t; if (!targetTransform) { return float.MaxValue; } var targetCollider = targetTransform.GetComponent(); if (!targetCollider) { return float.MaxValue; } var closestPoint = targetCollider.ClosestPoint(myPos); return Vector3.Distance(myPos, closestPoint); }) .FirstOrDefault(); if (nearestTarget == null) return; TargetTransform = (Transform)(object)nearestTarget; } public virtual void SetNearestTargetInDefense(Vector3 centerPos, float range) { Array.Clear(ColliderWithinRange, 0, TARGET_MAX_SIZE); var maxColliderCount = Physics.OverlapSphereNonAlloc(centerPos, range, ColliderWithinRange, TargetLayer, QueryTriggerInteraction.Collide); if (maxColliderCount <= 0) { TargetTransform = null; return; } 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; } 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 bool IsCommanded { get; set; } public void BeAttackedMovement(Vector3 attackPos) { switch (AiStat.DefenseType) { case EDefenseType.NONE: print("AiStat.DefenseType == NONE Error"); break; case EDefenseType.STRIKER: Utils.SetCloseDestination(navMeshAgent, attackPos, AiStat.AtkRange - 0.5f, AiStat.AtkRange); break; case EDefenseType.MIDFIELDER: Utils.SetCloseDestination(navMeshAgent, attackPos, AiStat.AtkRange - 0.5f, AiStat.AtkRange); break; case EDefenseType.DEFENDER: if (Vector3.Distance(DefensePos, attackPos) <= AiStat.DefenseRange) { Utils.SetCloseDestination(navMeshAgent, attackPos, AiStat.AtkRange - 0.5f, AiStat.AtkRange); } break; case EDefenseType.KEEPER: break; default: throw new ArgumentOutOfRangeException(); } } public void CommandMove(Vector3 targetPos) { StartCoroutine(CommandMoveCoroutine(targetPos)); } public IEnumerator CommandMoveCoroutine(Vector3 targetPos) { while (isAttacking) { yield return null; } if (Utils.SetCloseDestination(navMeshAgent, targetPos, 0.5f, 1f)) { IsCommanded = true; } while (navMeshAgent.pathPending || navMeshAgent.remainingDistance > navMeshAgent.stoppingDistance) { yield return CheckAgentArriveTime; } IsCommanded = false; } #endregion #endregion #region Custom function private void InitComponent() { 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); lookAtConstraint = Utils.GetComponentAndAssert(flagContainer); hitBoxCollider = Utils.GetComponentAndAssert(transform.Find("HitBox")); unitSelection = Utils.GetComponentAndAssert(UnitManager.Inst.transform); if (Camera.main != null) { var source = new ConstraintSource { sourceTransform = Camera.main.transform, weight = 1f }; lookAtConstraint.AddSource(source); } lookAtConstraint.constraintActive = true; } private void InitStart() { var getAiViewData = GetAiViewData(false); InitViewModel(false); FindMaterial(); SetLayer(); SetCloseWeapon(getAiViewData); SetCurrentHp(AiStat.MaxHp); SetMoveSpeed(AiStat.MoveSpd); DefensePos = transform.position; } private void InitViewModel(bool isEditor) { var getAiViewData = GetAiViewData(isEditor); SetActiveViewModel(backpackContainer, getAiViewData(AiStat.ViewIdx).Backpack); SetActiveViewModel(leftWeaponContainer, getAiViewData(AiStat.ViewIdx).LeftWeapon); SetActiveViewModel(leftShieldContainer, getAiViewData(AiStat.ViewIdx).LeftShield); SetActiveViewModel(headContainer, getAiViewData(AiStat.ViewIdx).Head); SetActiveViewModel(rightWeaponContainer, getAiViewData(AiStat.ViewIdx).RightWeapon); SetActiveViewModel(bodyContainer, getAiViewData(AiStat.ViewIdx).Body); SetActiveViewModel(flagContainer, getAiViewData(AiStat.ViewIdx).Flag); } private void SetCloseWeapon(GetAiViewDataDelegate getAiViewData) { if (getAiViewData(AiStat.ViewIdx).RightWeapon == -1) return; closeWeapon = rightWeaponContainer.GetChild(getAiViewData(AiStat.ViewIdx).RightWeapon).AddComponent(); closeWeapon.gameObject.layer = LayerMask.NameToLayer("Weapon"); //closeWeapon.SetAttackerAiType(AiStat.AiType); closeWeapon.SetBoxCollider(); } private GetAiViewDataDelegate GetAiViewData(bool isEditor) => isEditor ? DataManager.Inst.GetEnemyViewSoFromKey : DataManager.Inst.GetEnemyViewDictionaryFromKey; #if UNITY_EDITOR public void InitStartInEditor() { var getAiViewData = GetAiViewData(true); InitComponent(); InitViewModel(true); SetLayer(); SetCloseWeapon(getAiViewData); SetCurrentHp(AiStat.MaxHp); SetMoveSpeed(AiStat.MoveSpd); } #endif public void ExecuteFindTarget() { switch (AiStat.AttackerType) { case EAttackerType.NONE: print("AiStat.AttackerType == NONE Error"); break; case EAttackerType.OFFENSE: StartCoroutine(nameof(FindTargetInOffense)); break; case EAttackerType.DEFENSE: StartCoroutine(nameof(FindTargetInDefense)); break; default: throw new ArgumentOutOfRangeException(); } } 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); } } public virtual void Attack() { IsAttackCoroutine = true; StartCoroutine(nameof(AttackAnimation)); } protected virtual IEnumerator AttackAnimation() { closeWeapon.SetIsAttacked(false); closeWeapon.SetWeaponStat(AiStat.Atk, AiStat.ShieldPenetrationRate, AiStat.AttackerType == EAttackerType.OFFENSE); aiAnimator.SetTrigger(AttackHash); while (isAttacking) { yield return null; } yield return new WaitForSeconds(AiStat.AtkCooldown); IsAttackCoroutine = false; } protected virtual bool CanAttack() { switch (AiStat.AttackerType) { case EAttackerType.NONE: print("AiStat.AttackerType == NONE Error"); return false; case EAttackerType.OFFENSE: if (!TargetTransform || !islandInfo.TargetAllList.Contains(TargetTransform)) return false; break; case EAttackerType.DEFENSE: if (!TargetTransform) return false; break; default: throw new ArgumentOutOfRangeException(); } var targetInAttackRange = Vector3.Distance(transform.position, TargetTransform.position) <= AiStat.AtkRange; return targetInAttackRange; } 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 == null) return; islandInfo.RemoveListElement(islandInfo.EnemyList, transform); } private void SetAgentIsStopped(bool value) { if (navMeshAgent.enabled) { navMeshAgent.isStopped = value; } } protected virtual void SetLayer() { // switch (AiStat.AiType) // { // case EAiType.NONE: // print("AiStat.AiType == NONE Error"); // break; // case EAiType.PLAYER: // gameObject.layer = LayerMask.NameToLayer("Player"); // hitBoxCollider.gameObject.layer = LayerMask.NameToLayer("HitBox"); // hitBoxCollider.gameObject.tag = "Player"; // TargetLayer = LayerMask.GetMask("Enemy"); // break; // case EAiType.PIRATE: // gameObject.layer = LayerMask.NameToLayer("Pirate"); // hitBoxCollider.gameObject.layer = LayerMask.NameToLayer("HitBox"); // hitBoxCollider.gameObject.tag = "Pirate"; // TargetLayer = LayerMask.GetMask("Enemy"); // break; // case EAiType.ENEMY: // gameObject.layer = LayerMask.NameToLayer("Enemy"); // hitBoxCollider.gameObject.layer = LayerMask.NameToLayer("HitBox"); // hitBoxCollider.gameObject.tag = "Enemy"; // TargetLayer = LayerMask.GetMask("Player") | LayerMask.GetMask("Pirate"); // break; // default: // throw new ArgumentOutOfRangeException(); // } // // if (AiStat.AttackerType == EAttackerType.OFFENSE) // { // TargetLayer |= LayerMask.GetMask("Props"); // } } public IslandInfo GetIslandInfo() => islandInfo; public void SetIslandInfo(IslandInfo info) => islandInfo = info; public void SetAttackerType(EAttackerType type) => AiStat.AttackerType = type; public void SetOffenseType(EOffenseType type) => AiStat.OffenseType = type; public void SetDefenseType(EDefenseType type) => AiStat.DefenseType = type; public void SetCloseWeaponCanAttack(int boolValue) => closeWeapon.SetCanAttack(boolValue == 1); 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 } }