첫번째 Sequence에서 가장 마지막 검사에 Selector을 넣어서 일반 공격과 특수 공격을 관리했는데 새로운 구조로 변경.
아래 3가지 이유로 구조를 변경함.
쿨타임은 Coroutine으로 돌리고. bool값을 통해 스킬 사용 여부를 체크함
public class Enemy : MonoBehaviour
{
// Distance
[Header("Distnace")]
[SerializeField]
protected float _detectDistance;
[SerializeField]
protected float _attackDistance;
[SerializeField]
protected float _actionDistance;
// Movement
[Header("Movement")]
[SerializeField]
protected float _movementSpeed;
// CoolTime
[Header("CoolTime")]
protected bool _isCoolTime;
protected BehaviourTreeRunner _BTRunner = null;
protected Transform _detectedPlayer = null;
protected Vector3 _originPos = default;
protected Animator _animator = null;
protected Rigidbody2D _rigid;
protected RaycastHit2D[] hitData;
protected const string _ATTACK_ANIM_STATE_NAME = "Attack";
protected const string _ATTACK_ANIM_TIRGGER_NAME = "IsAttack";
public Enemy()
{
this._detectDistance = 0;
this._attackDistance = 0;
this._movementSpeed = 0;
}
protected virtual void Awake()
{
_BTRunner = new BehaviourTreeRunner(SettingBT());
_animator = GetComponentInChildren<Animator>();
_rigid = GetComponent<Rigidbody2D>();
_originPos = transform.position;
}
protected void Update()
{
_BTRunner.Operate();
}
protected virtual INode SettingBT()
{
return new SelectorNode
(
new List<INode>()
{
new SequenceNode
(
new List<INode>()
{
new ActionNode(CheckAttacking), // 공격중?
new ActionNode(CheckEnemyWithineAttackRange), // 공격 범위 안?
new ActionNode(DoAttack) // 공격
}
),
new SequenceNode
(
new List<INode>()
{
new ActionNode(CheckDetectEnemy), // 적 발견?
new ActionNode(MoveToDetectEnemy) // 적한테 이동
}
),
new ActionNode(MoveToOriginPosition) // 원래 자리로
}
);
}
protected bool IsAnimationRunning(string stateName)
{
if (_animator != null)
{
if (_animator.GetCurrentAnimatorStateInfo(0).IsName(stateName)) // (stateName) 애니메이션이 진행중인가?
{
var normalizedTime = _animator.GetCurrentAnimatorStateInfo(0).normalizedTime;
return normalizedTime != 0 && normalizedTime < 1f;
}
}
return false;
}
#region Attack Node
protected virtual INode.ENodeState CheckAttacking()
{
if (IsAnimationRunning(_ATTACK_ANIM_STATE_NAME))
{
return INode.ENodeState.ENS_Running;
}
return INode.ENodeState.ENS_Success;
}
protected INode.ENodeState CheckEnemyWithineAttackRange()
{
if (_detectedPlayer != null)
{
if (Vector3.SqrMagnitude(_detectedPlayer.position - transform.position) < (_attackDistance * _attackDistance)) // 제곱근(피타고라스)
{
return INode.ENodeState.ENS_Success;
}
}
return INode.ENodeState.ENS_Failure;
}
protected virtual INode.ENodeState DoAttack()
{
if (_detectedPlayer != null)
{
_animator.SetTrigger(_ATTACK_ANIM_TIRGGER_NAME);
return INode.ENodeState.ENS_Success;
}
return INode.ENodeState.ENS_Failure;
}
#endregion
#region Detect & Move Node
protected INode.ENodeState CheckDetectEnemy()
{
// var overlapColliders = Physics.OverlapSphere(transform.position, _detectDistance, LayerMask.GetMask("Player")); // OverlapSphere : 구 형태로 "주변 콜라이더" 감지 <= 3D
var overlapColliders = Physics2D.OverlapCircleAll(transform.position, _detectDistance, LayerMask.GetMask("Player"));
if (overlapColliders != null && overlapColliders.Length > 0)
{
_detectedPlayer = overlapColliders[0].transform;
return INode.ENodeState.ENS_Success;
}
_detectedPlayer = null;
return INode.ENodeState.ENS_Failure;
}
protected virtual INode.ENodeState MoveToDetectEnemy()
{
if (_detectedPlayer != null)
{
CheckPlayerRay();
if (hitData.Length > 1 && hitData[1].collider.CompareTag("Player"))
{
if (Vector3.SqrMagnitude(_detectedPlayer.position - transform.position) < (_attackDistance * _attackDistance))
{
return INode.ENodeState.ENS_Running;
}
transform.position = Vector3.MoveTowards(transform.position, _detectedPlayer.position, Time.deltaTime * _movementSpeed);
return INode.ENodeState.ENS_Running;
}
}
return INode.ENodeState.ENS_Failure;
}
#endregion
#region Move Origin Position Node
protected INode.ENodeState MoveToOriginPosition()
{
if (Vector3.SqrMagnitude(_originPos - transform.position) <= float.Epsilon * float.Epsilon) // Epsilon : 수학에서 매우 작은 수를 의미하는 기호
{
return INode.ENodeState.ENS_Success;
}
else
{
transform.position = Vector3.MoveTowards(transform.position, _originPos, Time.deltaTime * _movementSpeed);
return INode.ENodeState.ENS_Running;
}
}
#endregion
protected void CheckPlayerRay()
{
hitData = Physics2D.RaycastAll(_rigid.position, _detectedPlayer.position - transform.position, _detectDistance);
Debug.DrawRay(transform.position, _detectedPlayer.position - transform.position, new Color(1, 0, 0));
}
protected void OnDrawGizmos()
{
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(this.transform.position, _detectDistance);
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(this.transform.position, _attackDistance);
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(this.transform.position, _actionDistance);
}
}
public sealed class NearEnemyAI : Enemy
{
private float coolTime = 20f;
private bool isSpecialAttacking = false;
private const string _CRASH_ANIM_TRIGGER_NAME = "IsCrash";
public NearEnemyAI() : base()
{
this._detectDistance = 4;
this._attackDistance = 1;
this._movementSpeed = 3;
this._actionDistance = 3;
this._isCoolTime = true;
}
protected override INode SettingBT()
{
return new SelectorNode
(
new List<INode>()
{
new SequenceNode
(
new List<INode>()
{
new ActionNode(CheckAttacking), // 공격중?
new InverterNode
(
new List<INode>()
{
new SequenceNode
(
new List<INode>()
{
new ActionNode(CheckCoolTime), // 쿨타임 체크
new ActionNode(CheckSpecialAttackDistance), // 범위 안?
new ActionNode(SpecialAttack) // 특수 공격
}
)
}
),
new ActionNode(CheckEnemyWithineAttackRange), // 공격 범위 안?
new ActionNode(DoAttack) // 일반 공격
}
),
new SequenceNode
(
new List<INode>()
{
new ActionNode(CheckDetectEnemy), // 적 발견?
new ActionNode(MoveToDetectEnemy) // 적한테 이동
}
),
new ActionNode(MoveToOriginPosition) // 원래 자리로
}
);
}
#region SpecialAttack_Node
private INode.ENodeState CheckCoolTime()
{
if (_isCoolTime)
{
return INode.ENodeState.ENS_Success;
}
return INode.ENodeState.ENS_Failure;
}
private INode.ENodeState CheckSpecialAttackDistance()
{
if (_detectedPlayer != null)
{
if (Vector3.SqrMagnitude(_detectedPlayer.position - transform.position) < (_actionDistance * _actionDistance))
{
return INode.ENodeState.ENS_Success;
}
}
return INode.ENodeState.ENS_Failure;
}
private INode.ENodeState SpecialAttack()
{
if (_isCoolTime && _detectedPlayer != null)
{
CheckPlayerRay();
if (hitData.Length > 1 && hitData[1].collider.CompareTag("Player"))
{
StartCoroutine(CoolTime());
StartCoroutine(CrashAttack());
}
return INode.ENodeState.ENS_Running;
}
return INode.ENodeState.ENS_Failure;
}
#endregion
#region Near_Attack Node
protected override INode.ENodeState CheckAttacking()
{
if (IsAnimationRunning(_ATTACK_ANIM_STATE_NAME) || isSpecialAttacking)
{
return INode.ENodeState.ENS_Running;
}
return INode.ENodeState.ENS_Success;
}
protected override INode.ENodeState DoAttack()
{
if (_detectedPlayer != null)
{
if (_isCoolTime)
{
return INode.ENodeState.ENS_Failure;
}
_animator.SetTrigger(_ATTACK_ANIM_TIRGGER_NAME);
return INode.ENodeState.ENS_Success;
}
return INode.ENodeState.ENS_Failure;
}
#endregion
#region SpecialAttack_Logic
private IEnumerator CoolTime()
{
_isCoolTime = false;
WaitForFixedUpdate waitFrame = new WaitForFixedUpdate();
while (coolTime > 0.1f)
{
coolTime -= Time.deltaTime;
yield return waitFrame;
}
coolTime = 20f;
_isCoolTime = true;
}
private IEnumerator CrashAttack()
{
isSpecialAttacking = true;
// Crash Ready
_animator.SetTrigger(_CRASH_ANIM_TRIGGER_NAME);
yield return new WaitForSeconds(1.5f);
_rigid.AddForce((_detectedPlayer.position - transform.position) * 5, ForceMode2D.Impulse);
// Stun
_animator.SetBool("IsStun", true);
yield return new WaitForSeconds(3f);
_animator.SetBool("IsStun", false);
isSpecialAttacking = false;
}
#endregion
private void OnTriggerEnter2D(Collider2D collision)
{
if (isSpecialAttacking)
{
if (collision.gameObject.CompareTag("Wall"))
{
_rigid.velocity = new Vector2(0, 0);
}
if (collision.gameObject.CompareTag("Player"))
{
_rigid.velocity = new Vector2(0, 0);
// 데미지를 주는 로직
// 넉백 적용
}
}
}
}
순찰(Patrol) 기능을 추가해야 한다.
순찰은 몬스터가 추적이 중단되면 원래 자리로 돌아오고 그 후에 순찰을 진행한다. (bool 체크)
360도 방향중 랜덤으로 한 방향에 먼저 이동할 랜덤 거리 Random_Distance만큼 Ray를 쏘고 Ray에 걸리는 오브젝트가 없다면 이동. (Ray에 걸리는 오브젝트가 있다면 다시 랜덤으로 돌려 쏜다.)
목적지에 도달하면 n초간 대기 후 다시 반복.
원거리 몬스터 BT의 코드도 구현 해야한다.