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; protected bool isAttacking; protected AttackerType attackerType; protected Animator aiAnimator; protected NavMeshAgent navMeshAgent; private UnitController mouseEnterUnitController; private UnitSelection unitSelection; 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 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); 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(); 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) { // 회피 성공 체크 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 #region IAiMover [field: Space(10f)] [field: Title("AiMover")] [field: SerializeField] public MoveType MoveType { get; set; } [field: SerializeField] public bool IsCommanded { get; set; } public void UpdateMovement() { aiAnimator.SetFloat(SpeedHash, navMeshAgent.velocity.normalized.magnitude); if (IsCommanded || isAttacking) { } else { if (!TargetInfo.transform || MoveType is MoveType.NONE or MoveType.FIXED) return; navMeshAgent.SetDestination(TargetInfo.transform.position); } } public void MoveTarget(Vector3 targetPos) { IsCommanded = true; navMeshAgent.SetDestination(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(Color.black); public void MouseEnterHighlight() => SetOutlineColor(Color.white); public void SelectedHighlight() => SetOutlineColor(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 } }