[엔진 프로젝트]Enemy Walking Around with BT

2024. 12. 3. 01:39·Unity

적 구조를 짜둔 BT를 설명하기 앞서 에셋은 Behavior Desginer를 사용했음을 알린다.

https://assetstore.unity.com/packages/tools/visual-scripting/behavior-designer-behavior-trees-for-everyone-15277?srsltid=AfmBOopXb1yjcJMAv-zIJkzmU6y0K4EhlQYt1UPaeKHLiPW0htoa63Fb

 

Behavior Designer - Behavior Trees for Everyone | 비주얼 스크립팅 | Unity Asset Store

Get the Behavior Designer - Behavior Trees for Everyone package from Opsive and speed up your game development process. Find this & other 비주얼 스크립팅 options on the Unity Asset Store.

assetstore.unity.com

(유니티 6에 나온 트리도 써볼 생각있음, 조만간 수정해서 한번 더 올릴 듯)

 

BT 구조를 짤수 있는 방법은 무수히 많다 보통은 아래 두가지 방법을 사용한다.

BT는 한개에 Action에 Running이 걸리고 특정 상황에서 Abort Type이나 탈출 조건에 맞춰서 다른 Action으로 이동하는 구조와 Running을 사용하지 않고 매 프레임 마다 계속해서 Conditional들을 체크하며 상태를 변경해나가는 구조가 있다. 필자는 후자의 경우를 택하여서 BT 구조를 만들었다.

 

BT 구조 짜기

먼저 조건들을 생각해보자

 

죽었거나 뭔가에 맞아서 행동이 불가능한 상태를 먼저 체크 해줘야 한다.

 

그다음에 공격 사거리에 안에 들었는지 체크한다 사거리 안이라면? 공격이 가능한 상태 인지 체크한다 (쿨타임이 되었는가?, 대기 상태인가? 등) 만약 공격이 불가능 하다면?? 대기 상태에 돌입한다.

 

만약 사거리에 내에 들지 않았다면 타겟을 방향으로 이동을 해야한다. 이 걸 그림으로 정리 하면

 

 

대충 이런 느낌으로 나온다. Inverter는 결과값을 반대로 바꾸어주는 데코레이터이다.

 

이걸 실제 유니티에 적용해보자.

 

유니티 적용(with. Behavior Designer)

결과물은 이런식으로 나온다 파트별로 나눠서 설명하겠다.

 

일단 전체를 Selector로 묶어서 성공이 반환된다면 BT를 종료 하도록한다.

IsDead는 죽었는지 안 죽었는지를 체크하는 함수이다.

CanAction은 현재 행동이 가능한 상태인지를 반환하는 함수이다. Inverter를 쓰는 이유는 선술했듯 Sel를 사용했지 때문에 

Success를 반환해야 옆에는 ActionSel이 작동하지 않는다.

 

using BehaviorDesigner.Runtime.Tasks;
using UnityEngine;

public class CanAction : Conditional
{
    [SerializeField] private EnemyScript _sharedEnemy;
	private Enemy _enemy;

    public override void OnAwake()
    {
        base.OnAwake();
		_enemy = _sharedEnemy.Value;
    }

    public override TaskStatus OnUpdate()
	{
		if(_enemy.CanAction){
			return TaskStatus.Success;
		}
		else{
			return TaskStatus.Failure;
		}
	}
}
using UnityEngine;
using BehaviorDesigner.Runtime.Tasks;

public class IsDead : Conditional
{
    [SerializeField] private EnemyScript _sharedEnemy;
    private Enemy _enemy;

    public override void OnAwake()
    {
        base.OnAwake();
        _enemy = _sharedEnemy.Value;
    }

    public override TaskStatus OnUpdate()
    {
        if (_enemy.HealthCompo.IsDead)
        {
            return TaskStatus.Success;
        }
        else
        {
            return TaskStatus.Failure;
        }
    }
}

 

Attack 부분 Seq이다.

일단 가장 먼저 공격 사거리 내에 있는지 확인을 한다. 그다음 Sel로 들어가서 Recover와 Attack 중 하나라도 성공하면 반환하게 한다.

using UnityEngine;
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;

public class InRange : Conditional
{
	[SerializeField] private SharedFloat _radius;
	[SerializeField] private SharedLayerMask _whatIsPlayer;
	[SerializeField] private bool _showGizmos;

	public override TaskStatus OnUpdate()
	{
		bool isScucces = Physics.CheckSphere(Owner.transform.position, _radius.Value, _whatIsPlayer.Value);
		if(isScucces) return TaskStatus.Success;
		else return TaskStatus.Failure;
	}

	public override void OnDrawGizmos()
	{
		if(!_showGizmos) return;
		Gizmos.color = Color.white;
		Gizmos.DrawWireSphere(Owner.transform.position, _radius.Value);
	}
}

 

RecoverSeq이다.

먼저 쿨타임이 다되었는지 체크 한뒤 Inverter를 사용하여 반환값을 반대로 바꾸어준다. 쿨타임 중이라면 Recover를 해야하기 때문이다.

그다음 Animation을 가만히 있는 상태로 바꾸어주고 타겟을 바라보는 LookToTarget를 실행시켜준다. Stop Immediately는

정지하는 Action이다.

 

using UnityEngine;
using BehaviorDesigner.Runtime.Tasks;

public class IsAttackCooldown : Conditional
{
	[SerializeField] private EnemyScript _sharedEnemy;
	private Enemy _enemy;

    public override void OnAwake()
    {
        base.OnAwake();
		_enemy = _sharedEnemy.Value as Enemy;
    }

    public override TaskStatus OnUpdate()
	{
		if(_enemy.lastAttackTime + _enemy.GetStat().attackCooldown.GetValue() <= Time.time)
			return TaskStatus.Success;
		return TaskStatus.Failure;
	}
}
using UnityEngine;
using BehaviorDesigner.Runtime.Tasks;

public class LookToTarget : Action
{
	[SerializeField] private EnemyScript _sharedEnemy;
	[SerializeField] private Transform _targetTrm;

	private Enemy _enemy;

    public override void OnAwake()
    {
        _enemy = _sharedEnemy.Value as Enemy;
    }

    public override void OnStart()
	{
		if(_targetTrm == null)
			_targetTrm = _enemy.PlayerTrm;
	}

	public override TaskStatus OnUpdate()
	{
		_enemy.GetCompo<EnemyMovement>().LookToTarget(_targetTrm.position);
		return TaskStatus.Success;
	}
}
using UnityEngine;
using BehaviorDesigner.Runtime.Tasks;

public class StopImmediately : Action
{
	[SerializeField] private EnemyScript _sharedEnemy;
	private Enemy _enemy;

    public override void OnAwake()
    {
        base.OnAwake();
		_enemy = _sharedEnemy.Value as Enemy;
    }

    public override void OnStart()
	{
		_enemy.GetCompo<EnemyMovement>().StopImmediately();
	}
}

(SetBoolAnimator는 길어서 코드로는 안 남김)\

 

공격 Seq이다.

공격이 가능한지 체크한다. 그리고 Random으로 Int값을 가져와서 어떤 공격을 할지 정해준다. 그리고 애니메이션 실행해주고 실제 공격을 해준다.

 

using UnityEngine;
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;

public class CanAttack : Conditional
{
	[SerializeField] private SharedFloat _radius;
	[SerializeField] private EnemyScript _sharedEnemy;
	[SerializeField] private SharedLayerMask _whatIsPlayer;
	[SerializeField] private bool _showGizmos;
	private Enemy _enemy;

    public override void OnAwake()
    {
        base.OnAwake();
		_enemy = _sharedEnemy.Value;
    }

    public override void OnStart()
    {
        base.OnStart();
    }

    public override TaskStatus OnUpdate()
	{
		EnemyMovement movement = _enemy.GetCompo<EnemyMovement>();
		bool isScucces = Physics.CheckSphere(Owner.transform.position, _radius.Value, _whatIsPlayer.Value);
		bool cooldownPass = _enemy.lastAttackTime + _enemy.GetStat().attackCooldown.GetValue() <= Time.time;

		if(isScucces){
			movement.LookToTarget(_enemy.PlayerTrm.position);
			movement.StopImmediately();
			if(_enemy.CanAttack && cooldownPass) return TaskStatus.Success;
			else return TaskStatus.Failure;
		}
		return TaskStatus.Failure;
	}

	public override void OnDrawGizmos()
	{
		if(!_showGizmos) return;
		Gizmos.color = Color.red;
		Gizmos.DrawWireSphere(Owner.transform.position, _radius.Value);
	}
}
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
using UnityEngine;
using Random = UnityEngine.Random;

public class RandomAttackIndex : Action
{
    public SharedInt min;
    public SharedInt max;
    public SharedInt storeResult;

    public override void OnStart()
    {
        storeResult.Value = Random.Range(min.Value, max.Value + 1);
    }
}
using UnityEngine;
using BehaviorDesigner.Runtime.Tasks;

public class EnemyAttack : Action
{
	[SerializeField] private EnemyScript _sharedEnemy;
	private Enemy _enemy;

	public override void OnAwake()
	{
		_enemy = _sharedEnemy.Value;
	}

    public override void OnStart()
    {
        base.OnStart();
        EnemyMovement movement = _enemy.GetCompo<EnemyMovement>();
		movement.StopImmediately();
        movement.LookToTarget(_enemy.PlayerTrm.position);
        _enemy.CanAttack = false;
        _enemy.CanAction = false;
    }

    public override TaskStatus OnUpdate()
    {
        return TaskStatus.Success;
    }
}

 

이동 관련 Seq이다.

로직이 간단해서 그냥 이 상태로 설명하겠다. 먼저 일정거리 내로 들었다면 이동을 천천히 하고 경계 상태에 돌입한다. 따라서 거리내에 들었는지 체크하고 True 라면 걷게 만들고 아니라면 뛰게 만든다. 그뒤 Move 실행해준다. (Idle은 Bool Param, false로 하는 거임) 그리고 실질적 이동을 해주면 된다.

 

그럼 끝!

 

using UnityEngine;
using BehaviorDesigner.Runtime.Tasks;

public enum LJSMoveType{
	Walk, Chase
}

public class ChangeMoveType : Action
{
	[SerializeField] private LJSMoveType _moveType;
	[SerializeField] private EnemyScript _sharedEnemy;
	private Enemy _enemy;

    public override void OnAwake()
    {
        base.OnAwake();
		_enemy = _sharedEnemy.Value as Enemy;
    }

    public override TaskStatus OnUpdate()
	{
		if(_enemy.Slow) return TaskStatus.Success;
		EnemyStat stat = _enemy.GetStat();
		if(_moveType == LJSMoveType.Walk){
			_enemy.GetCompo<EnemyMovement>().NavAgentCompo.speed = stat.WalkSpeed.GetValue();
		}
		else{
			_enemy.GetCompo<EnemyMovement>().NavAgentCompo.speed = stat.ChaseSpeed.GetValue();
		}
		return TaskStatus.Success;
	}
}
using UnityEngine;
using BehaviorDesigner.Runtime.Tasks;
using PJH.Agent.Animation;

public class EnemyMove : Action
{
    [SerializeField] private EnemyScript _sharedEnemy;
    private Enemy _enemy;
    private Transform _playerTrm;

    public override void OnAwake()
    {
        base.OnAwake();
        _enemy = _sharedEnemy.Value;
    }

    public override void OnStart()
    {
        _playerTrm = _enemy.PlayerTrm;
        EnemyMovement movement = _enemy.GetCompo<EnemyMovement>();

        movement.SetDirectMovement(_playerTrm.position, true);

        // movement.LookToTarget(_enemy.MovementCompo.GetNextPathPoint());
    }

    public override TaskStatus OnUpdate()
    {
        return TaskStatus.Success;
    }
}

 

이렇게 하면 기본적인 적의 구조는 완성이다. 다음 파트에서 Walking Around를 넣어보겠다.

 

'Unity' 카테고리의 다른 글

[졸업작품, Unity] 결과창 제작  (0) 2025.03.06
[졸업 작품, Unity]Item System 제작  (0) 2025.03.04
[엔진 프로젝트]Enemy Walking Around with BT  (0) 2024.11.01
[Unity] (2024_10_15) 2D 플랫 포머 게임 개발일지(Pooling)  (2) 2024.10.17
[Unity] (2024_09_30) 2D 플랫 포머 게임 개발일지  (0) 2024.09.30
'Unity' 카테고리의 다른 글
  • [졸업작품, Unity] 결과창 제작
  • [졸업 작품, Unity]Item System 제작
  • [엔진 프로젝트]Enemy Walking Around with BT
  • [Unity] (2024_10_15) 2D 플랫 포머 게임 개발일지(Pooling)
HK1206
HK1206
고3 게임 개발자의 개발 일지
  • HK1206
    GGM-LJS
    HK1206
  • 전체
    오늘
    어제
    • 분류 전체보기 (25)
      • Unity (16)
      • Shader (1)
      • Editor (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
HK1206
[엔진 프로젝트]Enemy Walking Around with BT
상단으로

티스토리툴바