- 그룹 상호작용에 따른 하이라이트 효과 적용 1. 마우스 커서 올림 - 하얀색 외곽선 2. 그룹 선택 - 파란색 외곽선 - Arrow, Archer 코루틴 문제 구조 변경 - FieldOfView 인터페이스화 - new input system Unit Action 추가 - GameManager 슬로우모드(부대 선택시) 추가 - Ai 전체 프리팹 stat 수치 변경 - 원거리 이동 중에 공격시 멈추면서 공격하도록 변경 - Layer, ProjectSetting Physics 추가 및변경
350 lines
11 KiB
C#
350 lines
11 KiB
C#
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
|
|
{
|
|
#region Property and variable
|
|
|
|
protected bool isAttacking;
|
|
protected AttackerType attackerType;
|
|
|
|
[SerializeField] protected List<Material> skinMaterial = new(10);
|
|
protected Animator aiAnimator;
|
|
protected AiMover aiMover;
|
|
protected NavMeshAgent navMeshAgent;
|
|
|
|
private UnitController mouseEnterUnitController;
|
|
private UnitSelection unitSelection;
|
|
|
|
public static readonly int SpeedHash = Animator.StringToHash("Speed");
|
|
public static readonly int AttackHash = Animator.StringToHash("Attack");
|
|
public static readonly int DamageHash = Animator.StringToHash("TakeDamage");
|
|
public static readonly int DeathTypeHash = Animator.StringToHash("DeathType");
|
|
public static readonly int DeathHash = Animator.StringToHash("Death");
|
|
public 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<Animator>(transform);
|
|
aiMover = Utils.GetComponentAndAssert<AiMover>(transform);
|
|
navMeshAgent = Utils.GetComponentAndAssert<NavMeshAgent>(transform);
|
|
|
|
unitSelection = FindObjectOfType<UnitSelection>();
|
|
|
|
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();
|
|
}
|
|
|
|
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 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<TargetInfo> 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<IAiStat>();
|
|
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
|
|
|
|
#endregion
|
|
|
|
#region Custom function
|
|
private void FindMaterial()
|
|
{
|
|
var skinnedMeshRenderers = GetComponentsInChildren<SkinnedMeshRenderer>();
|
|
var meshRenderers = GetComponentsInChildren<MeshRenderer>();
|
|
|
|
foreach (var skin in skinnedMeshRenderers)
|
|
{
|
|
if (!skin.gameObject.activeSelf) continue;
|
|
|
|
skinMaterial.Add(skin.material);
|
|
}
|
|
|
|
foreach (var skin in meshRenderers)
|
|
{
|
|
if (!skin.gameObject.activeSelf) continue;
|
|
|
|
skinMaterial.Add(skin.material);
|
|
}
|
|
}
|
|
|
|
public void ResetHighlight()
|
|
{
|
|
foreach (var skin in skinMaterial)
|
|
{
|
|
skin.SetColor(OutlineColorHash, Color.black);
|
|
}
|
|
}
|
|
public void MouseEnterHighlight()
|
|
{
|
|
foreach (var skin in skinMaterial)
|
|
{
|
|
skin.SetColor(OutlineColorHash, Color.white);
|
|
}
|
|
}
|
|
|
|
public void SelectedHighlight()
|
|
{
|
|
foreach (var skin in skinMaterial)
|
|
{
|
|
skin.SetColor(OutlineColorHash, 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
|
|
}
|
|
} |