📌 Unity 심화 팀 프로젝트 (코드 정리)
📌 Node
➔ INode
public interface INode
{
public enum ENodeState
{
ENS_Running,
ENS_Success,
ENS_Failure
}
public ENodeState Evaluate();
}
➔ BehaniourTreeRunner
public class BehaviourTreeRunner
{
private INode _rootNode;
public BehaviourTreeRunner(INode rootNode)
{
_rootNode = rootNode;
}
public void Operate()
{
_rootNode.Evaluate();
}
}
➔ Selector
public sealed class SelectorNode : INode
{
private readonly List<INode> _childs;
public SelectorNode(List<INode> childs)
{
_childs = childs;
}
public INode.ENodeState Evaluate()
{
if (_childs == null)
return INode.ENodeState.ENS_Failure;
foreach (var child in _childs)
{
switch (child.Evaluate())
{
case INode.ENodeState.ENS_Running:
return INode.ENodeState.ENS_Running;
case INode.ENodeState.ENS_Success:
return INode.ENodeState.ENS_Success;
case INode.ENodeState.ENS_Failure:
continue;
}
}
return INode.ENodeState.ENS_Failure;
}
}
➔ Sequence
public sealed class SequenceNode : INode
{
private readonly List<INode> _childs;
public SequenceNode(List<INode> childs)
{
_childs = childs;
}
public INode.ENodeState Evaluate()
{
if (_childs == null || _childs.Count == 0)
return INode.ENodeState.ENS_Failure;
foreach (var child in _childs)
{
switch (child.Evaluate())
{
case INode.ENodeState.ENS_Running:
return INode.ENodeState.ENS_Running;
case INode.ENodeState.ENS_Success:
continue;
case INode.ENodeState.ENS_Failure:
return INode.ENodeState.ENS_Failure;
}
}
return INode.ENodeState.ENS_Failure;
}
}
➔ Action(Leaf)
public sealed class ActionNode : INode
{
private Func<INode.ENodeState> _onUpdate = null;
public ActionNode(Func<INode.ENodeState> onUpdate)
{
_onUpdate = onUpdate;
}
public INode.ENodeState Evaluate() => _onUpdate?.Invoke() ?? INode.ENodeState.ENS_Failure;
}
➔ Inverter
public sealed class InverterNode : INode
{
private readonly List<INode> _childs;
public InverterNode(List<INode> childs)
{
_childs = childs;
}
public INode.ENodeState Evaluate()
{
if (_childs == null || _childs.Count == 0)
return INode.ENodeState.ENS_Failure;
foreach (var child in _childs)
{
switch (child.Evaluate())
{
case INode.ENodeState.ENS_Running:
return INode.ENodeState.ENS_Running;
case INode.ENodeState.ENS_Success:
return INode.ENodeState.ENS_Failure;
case INode.ENodeState.ENS_Failure:
return INode.ENodeState.ENS_Success;
}
}
return INode.ENodeState.ENS_Running;
}
}
➔ Repeat
➔ UntilFail
📌 Basic BT
➔ EnemyBasicBT
public class EnemyBasicBT : MonoBehaviour
{
#region Global Variable
[Header("Distance")]
[SerializeField]
protected float _detectDistance;
[SerializeField]
protected float _attackDistance;
[SerializeField]
protected float _actionDistance;
[SerializeField]
protected float _patrolDistance;
[Header("Move")]
[SerializeField]
protected float _movementSpeed;
[Header("Time")]
[SerializeField]
protected float coolTime = 0;
protected bool _isCoolTime = true;
protected float _originCoolTime = 0;
[SerializeField]
protected float patrolReadyTime;
protected BehaviourTreeRunner _BTRunner = null;
protected Rigidbody2D _rigid;
protected Animator _animator = null;
protected SpriteRenderer _sprite;
protected RaycastHit2D[] hitData;
protected Transform _detectedPlayer = null;
protected Vector3 _originPos = default;
protected Vector2 randomPatrolPos = default;
protected bool _patrolMoveCheck = false;
protected const string _ATTACK_ANIM_STATE_NAME = "Attack";
protected const string _ATTACK_ANIM_Bool_NAME = "IsAttack";
protected const string _RUN_ANIM_STATE_NAME = "Run";
protected const string _RUN_ANIM_BOOL_NAME = "IsRun";
#endregion
#region Init Method
protected virtual void Awake()
{
_BTRunner = new BehaviourTreeRunner(SettingBT());
_animator = GetComponentInChildren<Animator>();
_rigid = GetComponent<Rigidbody2D>();
_sprite = GetComponentInChildren<SpriteRenderer>();
_originPos = transform.position;
patrolReadyTime = Random.Range(2f, 5f);
}
protected void Update()
{
_BTRunner.Operate();
}
private void InitStates()
{
this._detectDistance = 0;
this._attackDistance = 0;
this._movementSpeed = 0;
this._actionDistance = 0;
this.coolTime = 0f;
}
#endregion
#region BT Setting
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 SelectorNode
(
new List<INode>()
{
new SequenceNode
(
new List<INode>()
{
new ActionNode(RandomPatrolPositionCheck),
new ActionNode(MoveToPatrolPosition)
}
)
}
)
}
);
}
#endregion
#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.SetBool(_ATTACK_ANIM_Bool_NAME, true);
return INode.ENodeState.ENS_Success;
}
return INode.ENodeState.ENS_Failure;
}
#endregion
#region Detect & Move Node
protected INode.ENodeState CheckDetectEnemy()
{
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("Wall"))
{
if (Vector3.SqrMagnitude(_detectedPlayer.position - transform.position) < (_attackDistance * _attackDistance))
{
IdleAnimCheck();
return INode.ENodeState.ENS_Running;
}
_animator.SetBool(_ATTACK_ANIM_Bool_NAME, false);
RunAnimCheck();
FlipSprite(transform.position, _detectedPlayer.position);
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 & Patrol Node
protected INode.ENodeState MoveToOriginPosition()
{
if (Vector3.SqrMagnitude(_originPos - transform.position) <= float.Epsilon * float.Epsilon)
{
IdleAnimCheck();
return INode.ENodeState.ENS_Success;
}
else
{
RunAnimCheck();
FlipSprite(transform.position, _originPos);
transform.position = Vector3.MoveTowards(transform.position, _originPos, Time.deltaTime * _movementSpeed);
return INode.ENodeState.ENS_Running;
}
}
protected bool _patrolFirstCheck = true;
protected INode.ENodeState RandomPatrolPositionCheck()
{
if (!_patrolMoveCheck)
{
randomPatrolPos = Random.insideUnitCircle * patrolReadyTime;
hitData = Physics2D.RaycastAll(transform.position, randomPatrolPos, _patrolDistance);
_patrolMoveCheck = true;
}
Debug.DrawRay(transform.position, randomPatrolPos, new Color(1, 0, 0));
if (hitData.Length <= 1)
{
return INode.ENodeState.ENS_Success;
}
_patrolMoveCheck = false;
return INode.ENodeState.ENS_Failure;
}
protected INode.ENodeState MoveToPatrolPosition()
{
_animator.SetBool(_ATTACK_ANIM_Bool_NAME, false);
if (Vector3.SqrMagnitude((Vector3)randomPatrolPos - transform.position) <= float.Epsilon * float.Epsilon)
{
IdleAnimCheck();
if (_patrolFirstCheck) StartCoroutine(PatrolReadTime());
return INode.ENodeState.ENS_Failure;
}
else
{
RunAnimCheck();
FlipSprite(transform.position, randomPatrolPos);
transform.position = Vector3.MoveTowards(transform.position, randomPatrolPos, Time.deltaTime * _movementSpeed);
return INode.ENodeState.ENS_Running;
}
}
#endregion
#region Node Internal Functions
protected bool IsAnimationRunning(string stateName)
{
if (_animator != null)
{
if (_animator.GetCurrentAnimatorStateInfo(0).IsName(stateName))
{
var normalizedTime = _animator.GetCurrentAnimatorStateInfo(0).normalizedTime;
return normalizedTime != 0 && normalizedTime < 1f;
}
}
return false;
}
protected IEnumerator CoolTime()
{
_isCoolTime = false;
WaitForFixedUpdate waitFrame = new WaitForFixedUpdate();
while (coolTime > 0.1f)
{
coolTime -= Time.deltaTime;
yield return waitFrame;
}
coolTime = _originCoolTime;
_isCoolTime = true;
}
protected IEnumerator PatrolReadTime()
{
_patrolFirstCheck = false;
WaitForFixedUpdate waitFrames = new WaitForFixedUpdate();
while (patrolReadyTime > 0.1f)
{
patrolReadyTime -= Time.deltaTime;
yield return waitFrames;
}
patrolReadyTime = Random.Range(2f, 5f);
_patrolFirstCheck = true;
_patrolMoveCheck = false;
}
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 RunAnimCheck()
{
if (!_animator.GetBool(_RUN_ANIM_BOOL_NAME))
{
_animator.SetBool(_RUN_ANIM_BOOL_NAME, true);
}
}
protected void IdleAnimCheck()
{
if (_animator.GetBool(_RUN_ANIM_BOOL_NAME))
{
_animator.SetBool(_RUN_ANIM_BOOL_NAME, false);
}
}
protected void FlipSprite(Vector3 myPos, Vector3 targetPos)
{
Vector3 distance = (targetPos - myPos).normalized;
_sprite.flipX = distance.x < 0f;
}
#endregion
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);
}
}
➔ NearEnemyBT
public class NearEnemyBT : EnemyBasicBT
{
protected bool isSpecialAttacking = false;
protected override void Awake()
{
base.Awake();
}
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 SelectorNode
(
new List<INode>()
{
new SequenceNode
(
new List<INode>()
{
new ActionNode(RandomPatrolPositionCheck),
new ActionNode(MoveToPatrolPosition)
}
)
}
)
}
);
}
#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;
}
protected virtual INode.ENodeState SpecialAttack()
{
return INode.ENodeState.ENS_Failure;
}
#endregion
#region Near_Attack Node
protected override INode.ENodeState DoAttack()
{
if (_detectedPlayer != null)
{
if (_isCoolTime)
{
return INode.ENodeState.ENS_Failure;
}
FlipSprite(transform.position, _detectedPlayer.position);
_animator.SetBool(_ATTACK_ANIM_Bool_NAME, true);
return INode.ENodeState.ENS_Success;
}
return INode.ENodeState.ENS_Failure;
}
#endregion
}
➔ FarEnemyBT
public class FarEnemyBT : EnemyBasicBT
{
protected override void Awake()
{
base.Awake();
}
}
📌 Enemy Type
➔ NormalEnemy
public class NormalEnemy : NearEnemyBT
{
protected override void Awake()
{
base.Awake();
this._detectDistance = 5;
this._attackDistance = 1;
this._movementSpeed = 2;
this._actionDistance = 4;
this.coolTime = 0f;
this._originCoolTime = this.coolTime;
}
}
➔ NearTankerEnemy
public class NearTankerEnemy : NearEnemyBT
{
private const string _CRASH_ANIM_TRIGGER_NAME = "IsCrash";
protected override void Awake()
{
base.Awake();
this._detectDistance = 4;
this._attackDistance = 1;
this._movementSpeed = 3;
this._actionDistance = 3;
this.coolTime = 20f;
this._originCoolTime = this.coolTime;
}
#region Override Node Method
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 SpecialAttack()
{
if (_isCoolTime && _detectedPlayer != null)
{
IdleAnimCheck();
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 SpecialAttack_Logic
private IEnumerator CrashAttack()
{
isSpecialAttacking = true;
_animator.SetBool(_ATTACK_ANIM_Bool_NAME, false);
_animator.SetTrigger(_CRASH_ANIM_TRIGGER_NAME);
yield return new WaitForSeconds(1.5f);
FlipSprite(transform.position, _detectedPlayer.position);
_rigid.AddForce((_detectedPlayer.position - transform.position) * 5, ForceMode2D.Impulse);
_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);
}
}
}
}
➔ NearAssassinEnemy
public class NearAssassinEnemy : NearEnemyBT
{
protected override void Awake()
{
base.Awake();
this._detectDistance = 5;
this._attackDistance = 1;
this._movementSpeed = 3;
this._actionDistance = 4;
this.coolTime = 20f;
this._originCoolTime = this.coolTime;
}
#region Override Node Method
protected override INode.ENodeState SpecialAttack()
{
if (_isCoolTime && _detectedPlayer != null)
{
StartCoroutine(CoolTime());
AssassinAttack();
return INode.ENodeState.ENS_Success;
}
return INode.ENodeState.ENS_Failure;
}
#endregion
#region SpecialAttack_Logic
private void AssassinAttack()
{
if (_detectedPlayer != null)
{
Vector3 playerBackPos = _detectedPlayer.position - transform.position;
if (playerBackPos.x > 0)
{
transform.position = new Vector3(_detectedPlayer.position.x + 1f, _detectedPlayer.position.y, 0);
_sprite.flipX = true;
}
else
{
transform.position = new Vector3(_detectedPlayer.position.x - 1f, _detectedPlayer.position.y, 0);
_sprite.flipX = false;
}
_animator.SetBool(_ATTACK_ANIM_Bool_NAME, true);
}
}
#endregion
}