OldBlueWater/BlueWater/Assets/02.Scripts/Ai/AiController.cs
NTG 13cfeb3315 closed #9 근거리 Ai 공격, 충돌 테스트
#7 근거리 무기(MeleeWeapon) 추가
#8 부대 제어 수정 필요(기획 변경)

- Ai 버벅이던 현상 수정(Rigidbody interpolate 문제)
- UnitController 상세화(인스펙터창)
- 오펜스 관련 Ai 기본 설정
- Props 레이어 추가, House 태그 추가
- Physic 충돌 레이어 변경
- Ai 전체 프리팹 수정
- 테스트용 오펜스 ai 타겟 건물 추가
- Swordman 애니메이션 이벤트 누락 수정
2023-08-22 03:08:11 +09:00

583 lines
19 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.AI;
using Random = UnityEngine.Random;
// ReSharper disable once CheckNamespace
namespace BlueWaterProject
{
public enum AttackerType
{
NONE,
OFFENSE,
DEFENSE
}
public enum OffenseType
{
NONE,
NORMAL,
ONLY_HOUSE
}
public enum DefenseType
{
NONE,
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 = false;
[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<Material> 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;
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 Transform weaponLocation;
protected MeleeWeapon meleeWeapon;
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();
aiAnimator = Utils.GetComponentAndAssert<Animator>(transform);
navMeshAgent = Utils.GetComponentAndAssert<NavMeshAgent>(transform);
myUnitController = Utils.GetComponentAndAssert<UnitController>(transform.parent);
myCollider = Utils.GetComponentAndAssert<CapsuleCollider>(transform);
hitBoxCollider = Utils.GetComponentAndAssert<CapsuleCollider>(transform.Find("HitBox"));
unitSelection = FindObjectOfType<UnitSelection>();
SetAttackerType();
}
private void Start()
{
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<UnitController>();
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 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<T>(List<T> targetList)
{
if (targetList.Count <= 0) return;
var nearestTarget = targetList.OrderBy(t =>
{
var targetTransform = (Transform)(object)t;
var targetCollider = targetTransform.GetComponent<Collider>();
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
protected virtual void Attack()
{
StartCoroutine(nameof(AttackAnimation));
}
private IEnumerator AttackAnimation()
{
while (true)
{
if (!CanAttack())
{
isAttacking = false;
yield return FindTargetWaitTime;
continue;
}
isAttacking = true;
meleeWeapon.SetIsAttacked(false);
meleeWeapon.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<SkinnedMeshRenderer>();
var meshRenderers = GetComponentsInChildren<MeshRenderer>();
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 SetAttackerType()
{
if (gameObject.layer == LayerMask.NameToLayer("Player"))
{
attackerType = AttackerType.OFFENSE;
}
else if (gameObject.layer == LayerMask.NameToLayer("Pirate"))
{
attackerType = AttackerType.OFFENSE;
}
else if (gameObject.layer == LayerMask.NameToLayer("Enemy"))
{
attackerType = AttackerType.DEFENSE;
}
}
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
}
}