2023-10-11 20:26:45 +00:00
|
|
|
using System;
|
|
|
|
using System.Collections;
|
|
|
|
using BehaviorDesigner.Runtime;
|
|
|
|
using Sirenix.OdinInspector;
|
2023-09-27 01:29:45 +00:00
|
|
|
using UnityEngine;
|
2023-10-11 20:26:45 +00:00
|
|
|
using UnityEngine.AI;
|
2023-09-27 01:29:45 +00:00
|
|
|
|
|
|
|
// ReSharper disable once CheckNamespace
|
|
|
|
namespace BlueWaterProject
|
|
|
|
{
|
2023-10-11 20:26:45 +00:00
|
|
|
public abstract class Enemy : BaseCharacter, IDamageable, IAiView, IHelpCall
|
2023-09-27 01:29:45 +00:00
|
|
|
{
|
2023-10-11 20:26:45 +00:00
|
|
|
#region Properties and variables
|
|
|
|
|
|
|
|
[Title("DrawGizmos")]
|
|
|
|
[Tooltip("전체 Gizmos 그리기 여부")]
|
|
|
|
[SerializeField] private bool isDrawGizmos = true;
|
|
|
|
|
|
|
|
[ShowIf("@isDrawGizmos")]
|
|
|
|
[Tooltip("타겟 인식 범위 그리기 여부")]
|
|
|
|
[SerializeField] private bool isDrawViewRange = true;
|
|
|
|
|
|
|
|
[ShowIf("@isDrawGizmos")]
|
|
|
|
[Tooltip("이동제한 범위 그리기 여부")]
|
|
|
|
[SerializeField] private bool isDrawDefenseRange = true;
|
|
|
|
|
|
|
|
[ShowIf("@isDrawGizmos")]
|
|
|
|
[Tooltip("Idle 상태에서 랜덤으로 이동하는 범위 그리기 여부")]
|
|
|
|
[SerializeField] private bool isDrawRandomMoveRange = true;
|
|
|
|
|
|
|
|
[ShowIf("@isDrawGizmos")]
|
|
|
|
[Tooltip("타겟과의 상태 그리기 여부\n빨간색 = 공격 범위 밖\n파란색 = 공격 범위 안")]
|
|
|
|
[SerializeField] private bool isDrawTargetRange = true;
|
|
|
|
|
|
|
|
[field: Title("Stat")]
|
|
|
|
[field: Tooltip("행동 타입 설정")]
|
|
|
|
[field: SerializeField] public EBehaviorType BehaviorType { get; private set; } = EBehaviorType.DEFENDER;
|
|
|
|
|
|
|
|
[field: Tooltip("최대 체력 설정")]
|
|
|
|
[field: SerializeField] public float MaxHp { get; private set; } = 100f;
|
|
|
|
|
|
|
|
[field: Tooltip("현재 체력")]
|
|
|
|
[field: SerializeField] public float CurrentHp { get; private set; }
|
|
|
|
|
|
|
|
[field: Tooltip("이동 속도 설정")]
|
|
|
|
[field: SerializeField] public float MoveSpd { get; private set; } = 5f;
|
|
|
|
|
|
|
|
[field: Tooltip("공격력 설정")]
|
|
|
|
[field: SerializeField] public float Atk { get; private set; } = 10f;
|
|
|
|
|
|
|
|
[field: Tooltip("공격 속도(다음 공격 주기)\nAtkCooldown = 2f (2초마다 1번 공격)")]
|
|
|
|
[field: SerializeField] public float AtkCooldown { get; private set; } = 1f;
|
|
|
|
|
|
|
|
[field: Tooltip("공격 사거리 설정")]
|
2023-10-16 07:27:29 +00:00
|
|
|
[field: SerializeField] public float AtkRange { get; set; } = 1.5f;
|
2023-10-11 20:26:45 +00:00
|
|
|
|
|
|
|
[field: ShowIf("@BehaviorType == EBehaviorType.DEFENDER || BehaviorType == EBehaviorType.KEEPER")]
|
|
|
|
[field: Tooltip("이동 제한 범위 설정")]
|
|
|
|
[field: SerializeField] public float DefenseRange { get; private set; } = 20f;
|
|
|
|
|
|
|
|
[field: Tooltip("Idle 상태에서 랜덤으로 이동 여부")]
|
2023-10-16 07:27:29 +00:00
|
|
|
[field: SerializeField] public bool IsRandomMove { get; set; } = true;
|
2023-10-11 20:26:45 +00:00
|
|
|
|
|
|
|
[field: ShowIf("@IsRandomMove")]
|
|
|
|
[field: Tooltip("Idle 상태에서 이동하는 범위 설정")]
|
2023-10-16 07:27:29 +00:00
|
|
|
[field: SerializeField] public float RandomMoveRange { get; set; } = 5f;
|
2023-10-11 20:26:45 +00:00
|
|
|
|
|
|
|
[field: Title("Data")]
|
|
|
|
[field: DisableIf("@true")]
|
|
|
|
[field: SerializeField] public Vector3 DefensePos { get; set; }
|
|
|
|
[field: DisableIf("@true")]
|
|
|
|
[field: SerializeField] public bool IsCombated { get; set; }
|
|
|
|
|
|
|
|
[field: DisableIf("@true")]
|
|
|
|
[field: SerializeField] public bool BeAttackedInIdle { get; set; }
|
|
|
|
|
|
|
|
[DisableIf("@true")]
|
2023-10-16 07:27:29 +00:00
|
|
|
[SerializeField] protected bool beAttacked;
|
2023-10-11 20:26:45 +00:00
|
|
|
|
|
|
|
protected bool isAttacking;
|
|
|
|
|
|
|
|
// Component
|
|
|
|
protected Rigidbody rb;
|
|
|
|
public Collider MyCollider { get; private set; }
|
2023-10-16 07:27:29 +00:00
|
|
|
public NavMeshAgent Agent { get; set; }
|
2023-10-11 20:26:45 +00:00
|
|
|
protected BehaviorTree bt;
|
|
|
|
protected Animator myAnimator;
|
|
|
|
|
|
|
|
// Hash
|
|
|
|
protected static readonly int RunStateHash = Animator.StringToHash("RunState");
|
|
|
|
|
|
|
|
// Const
|
|
|
|
private static readonly WaitForSeconds BeAttackedWaitTime = new(0.3f);
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Unity built-in methods
|
|
|
|
|
|
|
|
protected override void OnDrawGizmosSelected()
|
|
|
|
{
|
|
|
|
base.OnDrawGizmosSelected();
|
|
|
|
|
|
|
|
if (!isDrawGizmos) return;
|
|
|
|
|
|
|
|
Vector3 myCenterPos;
|
|
|
|
Vector3 defensePos;
|
|
|
|
if (Application.isPlaying)
|
|
|
|
{
|
|
|
|
myCenterPos = MyCollider.bounds.center;
|
|
|
|
defensePos = DefensePos;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
myCenterPos = GetComponent<Collider>().bounds.center;
|
|
|
|
defensePos = transform.position;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (BehaviorType)
|
|
|
|
{
|
|
|
|
case EBehaviorType.NONE:
|
|
|
|
break;
|
|
|
|
case EBehaviorType.STRIKER:
|
|
|
|
break;
|
|
|
|
case EBehaviorType.DEFENDER:
|
|
|
|
case EBehaviorType.KEEPER:
|
|
|
|
if (isDrawDefenseRange)
|
|
|
|
{
|
|
|
|
Gizmos.color = Color.blue;
|
|
|
|
Gizmos.DrawWireSphere(defensePos, DefenseRange);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new ArgumentOutOfRangeException();
|
|
|
|
}
|
|
|
|
|
2023-10-17 07:31:10 +00:00
|
|
|
if (isDrawRandomMoveRange && IsRandomMove)
|
2023-10-11 20:26:45 +00:00
|
|
|
{
|
|
|
|
Gizmos.color = Color.green;
|
|
|
|
Gizmos.DrawWireSphere(defensePos, RandomMoveRange);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isDrawViewRange)
|
|
|
|
{
|
|
|
|
Gizmos.color = Color.red;
|
|
|
|
Gizmos.DrawWireSphere(myCenterPos, ViewRadius);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (UseHelpCall && IsDrawHelpCallRange)
|
|
|
|
{
|
|
|
|
Gizmos.color = Color.magenta;
|
|
|
|
Gizmos.DrawWireSphere(myCenterPos, HelpCallRange);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!Target || !isDrawTargetRange) return;
|
|
|
|
|
|
|
|
var targetToDistance = Vector3.Distance(myCenterPos, Target.bounds.center);
|
|
|
|
Gizmos.color = targetToDistance <= AtkRange ? Color.blue : Color.red;
|
|
|
|
Gizmos.DrawLine(myCenterPos, Target.bounds.center);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected override void Reset()
|
|
|
|
{
|
|
|
|
base.Reset();
|
|
|
|
|
|
|
|
isDrawGizmos = true;
|
|
|
|
isDrawViewRange = true;
|
|
|
|
isDrawDefenseRange = true;
|
|
|
|
isDrawRandomMoveRange = true;
|
|
|
|
isDrawTargetRange = true;
|
|
|
|
|
|
|
|
BehaviorType = EBehaviorType.DEFENDER;
|
|
|
|
MaxHp = 100f;
|
|
|
|
MoveSpd = 5f;
|
|
|
|
AtkCooldown = 1f;
|
|
|
|
AtkRange = 1.5f;
|
|
|
|
DefenseRange = 20f;
|
|
|
|
IsRandomMove = true;
|
|
|
|
RandomMoveRange = 5f;
|
|
|
|
ViewRadius = 15f;
|
|
|
|
UseHelpCall = false;
|
|
|
|
HelpLayer = LayerMask.GetMask("Enemy");
|
|
|
|
HelpCallRange = 15f;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected override void Awake()
|
|
|
|
{
|
|
|
|
base.Awake();
|
|
|
|
|
|
|
|
rb = GetComponent<Rigidbody>();
|
|
|
|
MyCollider = GetComponent<Collider>();
|
|
|
|
Agent = GetComponent<NavMeshAgent>();
|
|
|
|
bt = GetComponent<BehaviorTree>();
|
|
|
|
|
|
|
|
myAnimator = transform.Find("UnitRoot")?.GetComponent<Animator>();
|
|
|
|
if (myAnimator == null)
|
|
|
|
{
|
|
|
|
print("UnitRoot오브젝트를 찾을 수 없거나, Animator컴포넌트가 존재하지 않습니다.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected override void Start()
|
|
|
|
{
|
|
|
|
base.Start();
|
|
|
|
|
|
|
|
HelpLayer = LayerMask.GetMask("Enemy");
|
2023-10-17 07:31:10 +00:00
|
|
|
TargetLayer = LayerMask.GetMask("Player") | LayerMask.GetMask("Crewmate");
|
2023-10-11 20:26:45 +00:00
|
|
|
|
|
|
|
Agent.updateRotation = false;
|
|
|
|
DefensePos = transform.position;
|
|
|
|
SetAgentSpeed(ESpeedType.DEFAULT);
|
|
|
|
SetCurrentHp(MaxHp);
|
|
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Interface
|
|
|
|
|
|
|
|
// IDamageable
|
|
|
|
public virtual void TakeDamage(float attackerPower, float attackerShieldPenetrationRate = default, Vector3? attackPos = null)
|
|
|
|
{
|
|
|
|
IsCombated = true;
|
|
|
|
|
|
|
|
if (!Target)
|
|
|
|
{
|
|
|
|
BeAttackedInIdle = true;
|
|
|
|
bt.SendEvent("BeAttackedInIdle", attackPos);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (UseHelpCall)
|
|
|
|
{
|
|
|
|
HelpCall();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var changeHp = Mathf.Max(CurrentHp - attackerPower, 0);
|
|
|
|
SetCurrentHp(changeHp);
|
|
|
|
|
|
|
|
// 죽었는지 체크
|
|
|
|
if (changeHp == 0f)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-10-12 06:32:02 +00:00
|
|
|
StartCoroutine(nameof(BeAttacked));
|
2023-10-11 20:26:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// IAiView
|
|
|
|
[field: Title("IAiView")]
|
|
|
|
[field: SerializeField]
|
|
|
|
public float ViewRadius { get; set; } = 15f;
|
|
|
|
[field: SerializeField] public Collider[] Targets { get; set; } = new Collider[MAX_COLLIDERS];
|
|
|
|
[field: SerializeField] public Collider Target { get; set; }
|
|
|
|
[field: SerializeField] public LayerMask TargetLayer { get; set; }
|
|
|
|
|
|
|
|
private const int MAX_COLLIDERS = 30;
|
|
|
|
|
|
|
|
public void FindNearestTargetInRange(bool targetIsTrigger = true)
|
|
|
|
{
|
|
|
|
Array.Clear(Targets, 0, MAX_COLLIDERS);
|
|
|
|
|
|
|
|
var myCenterPos = MyCollider.bounds.center;
|
|
|
|
var numResults = Physics.OverlapSphereNonAlloc(myCenterPos, ViewRadius, Targets, TargetLayer,
|
|
|
|
targetIsTrigger ? QueryTriggerInteraction.Collide : QueryTriggerInteraction.Ignore);
|
|
|
|
|
|
|
|
if (numResults <= 0)
|
|
|
|
{
|
|
|
|
SetTarget(null);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var nearestDistance = ViewRadius * ViewRadius;
|
|
|
|
Collider nearestTargetCollider = null;
|
|
|
|
|
|
|
|
for (var i = 0; i < numResults; i++)
|
|
|
|
{
|
|
|
|
var distanceSqrToTarget = (myCenterPos - Targets[i].bounds.center).sqrMagnitude;
|
|
|
|
|
|
|
|
if (distanceSqrToTarget >= nearestDistance) continue;
|
|
|
|
|
|
|
|
nearestDistance = distanceSqrToTarget;
|
|
|
|
nearestTargetCollider = Targets[i];
|
|
|
|
}
|
|
|
|
|
|
|
|
SetTarget(nearestTargetCollider);
|
|
|
|
}
|
|
|
|
|
2023-10-16 07:27:29 +00:00
|
|
|
public void SetTarget(Collider value)
|
2023-10-11 20:26:45 +00:00
|
|
|
{
|
2023-10-16 07:27:29 +00:00
|
|
|
Target = value;
|
2023-10-11 20:26:45 +00:00
|
|
|
|
2023-10-16 07:27:29 +00:00
|
|
|
if (value != null)
|
|
|
|
{
|
|
|
|
IsCombated = true;
|
|
|
|
BeAttackedInIdle = false;
|
|
|
|
}
|
2023-10-11 20:26:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public bool IsTargetWithinRange()
|
|
|
|
{
|
|
|
|
var attackInRange = Vector3.Distance(MyCollider.bounds.center, Target.bounds.center) <= AtkRange;
|
|
|
|
return attackInRange;
|
|
|
|
}
|
|
|
|
|
|
|
|
public bool GoOutOfBounds()
|
|
|
|
{
|
|
|
|
if (BehaviorType != EBehaviorType.DEFENDER && BehaviorType != EBehaviorType.KEEPER) return false;
|
|
|
|
|
|
|
|
var defensePosInRange = Vector3.Distance(transform.position, DefensePos) <= DefenseRange;
|
|
|
|
return !defensePosInRange;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void MoveTarget(Vector3 targetPos, ESpeedType speedType, float stopDistance)
|
|
|
|
{
|
|
|
|
switch (BehaviorType)
|
|
|
|
{
|
|
|
|
case EBehaviorType.NONE:
|
|
|
|
print("BehaviorType == NONE error");
|
|
|
|
break;
|
|
|
|
case EBehaviorType.STRIKER:
|
|
|
|
case EBehaviorType.DEFENDER:
|
|
|
|
break;
|
|
|
|
case EBehaviorType.KEEPER:
|
|
|
|
return;
|
|
|
|
default:
|
|
|
|
throw new ArgumentOutOfRangeException();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Vector3.Distance(Agent.destination, targetPos) < 0.1f) return;
|
|
|
|
|
|
|
|
SetAgentSpeed(speedType);
|
|
|
|
Agent.stoppingDistance = stopDistance;
|
|
|
|
Agent.isStopped = false;
|
|
|
|
Agent.SetDestination(targetPos);
|
|
|
|
}
|
2023-10-16 07:27:29 +00:00
|
|
|
|
|
|
|
// IHelpCall
|
|
|
|
[field: Title("IHelpCall")]
|
|
|
|
[field: Tooltip("주변 아군에게 도움 요청")]
|
|
|
|
[field: SerializeField] public bool UseHelpCall { get; set; }
|
|
|
|
|
|
|
|
[field: ShowIf("@UseHelpCall && isDrawGizmos")]
|
|
|
|
[field: Tooltip("도움 요청 범위 그리기 여부")]
|
|
|
|
[field: SerializeField] public bool IsDrawHelpCallRange { get; set; }
|
|
|
|
|
|
|
|
[field: ShowIf("@UseHelpCall")]
|
|
|
|
[field: Tooltip("도움 요청 범위 설정")]
|
|
|
|
[field: SerializeField] public LayerMask HelpLayer { get; set; }
|
|
|
|
|
|
|
|
[field: ShowIf("@UseHelpCall")]
|
|
|
|
[field: Tooltip("도움 요청 범위 설정")]
|
|
|
|
[field: SerializeField] public float HelpCallRange { get; set; } = 15f;
|
|
|
|
|
|
|
|
[field: ShowIf("@UseHelpCall")]
|
|
|
|
[field: Tooltip("도움 요청 받은 아군 목록")]
|
|
|
|
[field: SerializeField] public Collider[] HelpTargets { get; set; } = new Collider[MAX_COLLIDERS];
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Custom methods
|
|
|
|
|
|
|
|
private IEnumerator BeAttacked()
|
|
|
|
{
|
|
|
|
beAttacked = true;
|
|
|
|
myAnimator.SetFloat(RunStateHash, 1f);
|
|
|
|
yield return BeAttackedWaitTime;
|
|
|
|
|
|
|
|
beAttacked = false;
|
|
|
|
}
|
2023-10-11 20:26:45 +00:00
|
|
|
|
|
|
|
public void HelpCall(bool targetIsTrigger = true)
|
|
|
|
{
|
|
|
|
Array.Clear(HelpTargets, 0, MAX_COLLIDERS);
|
|
|
|
|
|
|
|
var myCenterPos = MyCollider.bounds.center;
|
2023-10-12 06:32:02 +00:00
|
|
|
var numResults = Physics.OverlapSphereNonAlloc(myCenterPos, HelpCallRange, HelpTargets, HelpLayer,
|
2023-10-11 20:26:45 +00:00
|
|
|
targetIsTrigger ? QueryTriggerInteraction.Collide : QueryTriggerInteraction.Ignore);
|
|
|
|
|
|
|
|
for (var i = 0; i < numResults; i++)
|
|
|
|
{
|
|
|
|
var iHelpCall = HelpTargets[i].GetComponent<IHelpCall>();
|
|
|
|
|
|
|
|
if (iHelpCall == null || iHelpCall.Target != null) continue;
|
|
|
|
|
|
|
|
iHelpCall.SetTarget(Target);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void SetAgentSpeed(ESpeedType speedType)
|
|
|
|
{
|
|
|
|
switch (speedType)
|
|
|
|
{
|
|
|
|
case ESpeedType.NONE:
|
|
|
|
print("speedType == NONE error");
|
|
|
|
break;
|
|
|
|
case ESpeedType.DEFAULT:
|
|
|
|
Agent.speed = MoveSpd;
|
|
|
|
break;
|
|
|
|
case ESpeedType.SLOW:
|
|
|
|
Agent.speed = MoveSpd * 0.5f;
|
|
|
|
break;
|
|
|
|
case ESpeedType.FAST:
|
|
|
|
Agent.speed = MoveSpd * 2f;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(speedType), speedType, null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void SetCurrentHp(float value) => CurrentHp = value;
|
|
|
|
|
|
|
|
#endregion
|
2023-09-27 01:29:45 +00:00
|
|
|
}
|
|
|
|
}
|