행동 트리
행동 트리는 인공지능 모델을 설계하는데 많이 사용한다. 우선 순위와 트리 구조를 사용해서 인공지능을 설계하는 기법을 의미한다.
게임에서는 몬스터나 NPC의 행동을 결정하는 AI를 제작하는데 사용한다.
행동트리는 기존에 있던 FSM 모델의 문제를 해결했다.
이는 인공지능으로 활용하기에는 여러 상황에 대응하기가 어렵다는것이 일반적이다.
그래서 이를 개편해서 사용한 것이 Behavior Tree이다.
점이라 한다면.
- 모듈화가 잘 되어있어서 확장이 자유로움 애초에 가지를 가지고서 놀아주는 구조다 보니까,, 좁게할 수도 있고 넓게 할 수도 있음.
- 트리를 기반으로 계층화가 잘 되어있어서, 복잡한 인공지능 모델을 쉽게 설계 할 수 있다.
- 직관적으로 트리를 이용하다보니까, 손쉽게 파악이 가능함.
- BT에서 제공하는거 적절하게 활용하면 다양한 상황에 대응이 가능함.
행동 트리 모델의 구성 요소
행동 트리 역시 트리이기 때문에 항상 Root Node가 존재한다.
이 루트로부터의 의사결정을 진행한다.
왼쪽에 배치한 노드를 먼저 처리하는 방법으로 설정이 되어있다.
시작 상태를 설정할 필요 없이 그냥 깊이 우선 탐색을 이용하면 된다.
행동 트리는 행동을 중심으로 설계한다.
다만 이 행동이 독단적으로 실행이 되는건 아니고 부모 노드에서 다수의 행동을 컨트롤 하는 구조로 진행이 된다.
이것을 Composite라고 한다.
무조건 이 친구를 통해서만 실행이 가능하다.
Composite에는 여러 기능이 탑재되어 있다.
- Selector (여러 행동 중 하나를 선택하는 것)
- Sequence (여러 행동을 모두 수행하는 방법 ⇒ 차례로 수행)
- Parallel (여러 행동을 함께 수행하는 방법 ⇒ 병렬적으로 수행)
- 성공 : 성공함 ⇒ 완료 + 성공
- 실패 : 실패함 ⇒ 완료를 했는데 실패함.
- 중지 : 하다가 멈춤 ⇒ 하다가 갑자기 중지되었음.
- 진행 중 : 행동 결과를 홀딩한다. ⇒ 아직 행동이 안 끝났고 계속 진행 중!
이 결과를 Composite 노드에 전달한다.
근데 이 컴포짓은 행동의 결과에 따라서 다른 처리를 한다.
Selector의 경우에는 여러 행동 중에 하나만 선택하는 것이다 보니까.
⇒ 하나라도 성공하면 굳이 다른데로 갈 이유는 없음.
Sequence의 경우에는 순서대로 쪼로로로 실행하는 것이다 보니까.
⇒ 하나라도 실패한다면 다음으로 넘어가지 않는 것이 바람직함
추가적으로 Composite에는 제한을 걸 수도 있다.
- 데코레이터 : 컴포짓 노드가 실행되는 조건을 지정한다
- 서비스 : 컴포짓 노드가 활성화 될 때 주기적으로 실행하는 부가 명령
- 관찰자 중단 : 데코레이터 조건에 부합되면 컴포짓 내 활동을 모두 중단한다.
데코레이터의 경우에는 컴포짓에다가 조건을 달아서, 해당 조건이 만족이 되었을 시에만, 컴포짓이 실행되도록 만드는 것.
예시가 하나 더 있는데, 예를 들어서
데코레이터의 경우에는 특정 조건 하에서만 들어갈 수 있으므로, 공휴일에 장을 보러가는 참사는 막을 수 있음
서비스는 Composite의 기능이 수행하고 있는 동안에 주기적으로 수행을 하는 것을 의미함.
(뭐 Timer 로 생각하면 마음이 편하려나?)
여기서는 예를 들어서 2시간마다 시험을 친다고 하면 30분마다 시간을 알려주는 것과 동일.
관찰자 중단은 컴포짓을 진행하다가 어떤 특정 이벤트가 수행되면, 즉시 멈추고서 처음부터 행동 트리를 루트에서부터 재실행 할 시 유용하게 사용이 가능하다.
예를 들어서 긴급한 연락이 올 때까지 행동을 중지한다는 것을 개념적으로 본다고 하면,
관찰자 중단이 없다면 이 기능을 구현할 수가 없음..
근데 관찰자 중단이 있다면, 멈춘담에, 루트 노드로 가서 다른 일을 수행할 수 있게 된다.
예시로는 이렇게 가능하다
NPC 행동 트리
우리가 지금 조작하고 있는 캐릭터는 플레이어 컨트롤러가 빙의해서 키보드의 입력에 맞게끔 이를 바꿔주는 것인데 NPC는 아무 컨트롤러도 없기에 조작할 수 없다. 따라서 NPC에 맞는 AIController를 만들어 주고 여기에서 지정한 BT를 실행하도록 하자.
AABCharacterNonPlayer::AABCharacterNonPlayer()
{
GetMesh()->SetHiddenInGame(true);
AIControllerClass = AABAIController::StaticClass();
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}
행동트리를 하기 위해선 Blackboard라고 하는 엔티티가 필요하다. 이는 AI 모델에서 의사결정을 하기 위한 기본 데이터를 제공해주는 데이터 저장소이다.
여기에 다양한 데이터를 넣고 이 데이터를 기반으로 BT에서 결정한다.
UCLASS()
class ARENABATTLE_API AABAIController : public AAIController
{
GENERATED_BODY()
public :
AABAIController();
void RunAI();
void StopAI();
protected:
//폰에 빙의를 하였다면 수행되는 함수를 의미한다.
virtual void OnPossess(APawn* InPawn) override;
private:
UPROPERTY()
TObjectPtr<class UBlackboardData> BBAsset;
UPROPERTY()
TObjectPtr<class UBehaviourTree> BTAsset;
};
AABAIController::AABAIController()
{
static ConstructorHelpers::FObjectFinder<UBlackboardData> BBAssetRef(TEXT("/Script/AIModule.BlackboardData'/Game/ArenaBattle/AI/BB_ABCharacter.BB_ABCharacter'"));
if (BBAssetRef.Object) {
BBAsset = BBAssetRef.Object;
}
static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTAssetRef(TEXT("/Script/AIModule.BehaviorTree'/Game/ArenaBattle/AI/BT_ABCharacter.BT_ABCharacter'"));
if (BTAssetRef.Object) {
BTAsset = BTAssetRef.Object;
}
}
void AABAIController::RunAI()
{
UBlackboardComponent* BlackboardPtr = Blackboard.Get();
if (UseBlackboard(BBAsset, BlackboardPtr))
{
bool RunResult = RunBehaviorTree(BTAsset);
ensure(RunResult);
}
}
void AABAIController::StopAI()
{
UBehaviorTreeComponent* BTComponent = Cast<UBehaviorTreeComponent>(BrainComponent);
if (BTComponent) {
BTComponent->StopTree();
}
}
void AABAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
RunAI();
}
NPC 구현해보기
이제 위에서 다룬 개념을 가지고 직접 구현해보겠다.
블랙보드에 데이터를 넣어줄 수 있다.
이는 우리가 코드에서 설정해준 코드를 BT에서 사용할 수 있게 해주는 역할을 한다.
NPC가 이동할 수 있게 길찾기 기능을 추가해줘야 하는데 이는 NavigationMesh라고 한다.
여기서 설치한 후 Detail 조금 조정해주면 다음과 같이 나온다.
근데 문제는 우리는 맵을 계속해서 생성해 나갈 것이고 지금 만든 길은 static 으로만 동작하기 때문에
Project Setting에서 이 값을 수정해주어야 한다.
이걸 Dynamic으로 바꾸면 된다.
BT 설정하기
특정한 지점으로 움직이는 것은 Move To 라고 하는 Node가 존재한다. 이를 활용해 이렇게 세팅한다.
블랙보드에서 설정한 PatrolPos를 설정해주면 Sequence에 따라 Wait한 후 그 좌표로 Move To 하게 된다. 다만 지금 PatrolPos를 정해주는 노드가 존재하지 않기 때문에 그 값을 세팅해주는 Node를 만들어보자.
BTTask_FindPatrolPos 는 BTTaskNode를 상속받는데 ExecuteTask 함수를 override 해서 사용한다.
EBTNodeResult::Type UBTTask_FindPatrolPos::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);
APawn* ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
if (!ControllingPawn)
{
return EBTNodeResult::Failed;
}
UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(ControllingPawn->GetWorld());
if (nullptr == NavSystem)
{
return EBTNodeResult::Failed;
}
FVector Origin = OwnerComp.GetBlackboardComponent()->GetValueAsVector(BBKEY_HOMEPOS);
FNavLocation NextPatrolPos;
if(NavSystem->GetRandomPointInNavigableRadius(Origin, 500.0f, NextPatrolPos))
{
OwnerComp.GetBlackboardComponent()->SetValueAsVector(BBKEY_PATROLPOS, NextPatrolPos);
return EBTNodeResult::Succeeded;
}
return EBTNodeResult::Failed;
}
어찌되었던 Task의 경우에는 진행에 따라서 Failed인지 Succeed인지 판단할 필요가 있다.
- Task를 실행하려고 하는데, 이제 Component 자체에 Pawn이 존재하지 않으면 움직일 수가 없으므로 당연히 체크를 거친 뒤 Fail을 호출한다
- 다음으로는 System인데, 만약에 Navigation을 담당해주는 시스템 자체가 월드 내부적으로 존재하지 않으면 길찾기 자체가 불가능하므로 Failed를 쏴준다.다만 좀 이름이 특이한테 업데이팅을 거쳤기 때문에 다음과 같이 이름이 지정된 것. (이 때 UNavigationSystemV1을 이용하는데, 이것은 월드 자체에서 가져올 수 있다.)
- 다음으로 NavigationSystem에서 반경을 기준으로 해서 다음 위치를 랜덤으로 지정한다. 이 때 Origin의 값은 BlackBoardComponent에서 키값을 기준으로 가져와서 판단을 진행하도록 만들어준다.
- 다음으로 값을 성공적으로 가져왔다면, PatrolPos에 할당을 해주고, 이제 값을 정상적으로 뽑아왔다는 것을 Succeed를 호출하여 종료한다.
만든 Task를 추가해주면 정상적으로 잘 돌아다닌다.
주변 플레이어 감지하기
주변 플레이어를 감지하기 위해선 Service를 이용할 것이다. Service는 특정 시간을 깆ㄴ으로 해서 지속적으로 Task를 수행하는 구조로 동작한다.
TickNode함수를 실행하는 것이다.
UBTService_Detect::UBTService_Detect()
{
NodeName = TEXT("Detect");
Interval = 1.0f;
}
void UBTService_Detect::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
//폰의 정보를 가져온다
APawn* ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
if (nullptr == ControllingPawn)
{
return;
}
//현재 해당 컨트롤러의 위치를 가져오고, 해당 Pawn이 동작하고 있는 월드를 가져오는데,
//만약 미동작을 하는 경우에는 반환을 시켜줘야겠지.
FVector Center = ControllingPawn->GetActorLocation();
UWorld* World = ControllingPawn->GetWorld();
if (nullptr == World)
{
return;
}
//그리고 타입 캐스팅을 진행한 뒤,
IABCharacterAIInterface* AIPawn = Cast<IABCharacterAIInterface>(ControllingPawn);
if (nullptr == AIPawn)
{
return;
}
//추격 범위를 가져와서, 범위 내로 충돌하면 쫓아오게 만들어본다.
float DetectRadius = AIPawn->GetAIDetectRange();
TArray<FOverlapResult> OverlapResults;
FCollisionQueryParams CollisionQueryParam(SCENE_QUERY_STAT(Detect), false, ControllingPawn);
bool bResult = World->OverlapMultiByChannel(
OverlapResults,
Center,
FQuat::Identity,
CCHANNEL_ABACTION,
FCollisionShape::MakeSphere(DetectRadius),
CollisionQueryParam
);
if (bResult)
{
for (auto const& OverlapResult : OverlapResults)
{
APawn* Pawn = Cast<APawn>(OverlapResult.GetActor());
if (Pawn && Pawn->GetController()->IsPlayerController())
{
OwnerComp.GetBlackboardComponent()->SetValueAsObject(BBKEY_TARGET, Pawn);
DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Green, false, 0.2f);
DrawDebugPoint(World, Pawn->GetActorLocation(), 10.0f, FColor::Green, false, 0.2f);
DrawDebugLine(World, ControllingPawn->GetActorLocation(), Pawn->GetActorLocation(), FColor::Green, false, 0.27f);
return;
}
}
}
OwnerComp.GetBlackboardComponent()->SetValueAsObject(BBKEY_TARGET, nullptr);
DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Red, false, 0.2f);
}
- 현재 제어를 담당하고 있는 폰의 정보를 가져온다. → 폰이 없으면 Return
- 폰의 위치와 폰이 속한 월드를 가져온다.
- 형 변환을 거쳐서 해당 폰에 대해서 데이터를 가져올 수 있게 만들어 준다.
- Overlap함수를 이용해서 반경 내부적으로 들어왔을 시 판단하게 만들어 준다. 여기서 특이점은 플레이어가 다수라는 것을 가정해서 결과값이 Array로 되어있다는 것!
- 그 다음 감지가 된 것들을 기준으로 해서 검사를 하게 되는데, 만약에 해당 폰을 조종하고 있는 것이 플레이어라고 한다면, 이제 해당 타겟을 지정해서 플레이어의 위치와 Vector을 표현하게 될 것임.
그 다음 당연히 저기에서 사용한 DetectRange에 대한 값 반환을 해줘야 함.
현재 프로젝트에서는 400으로 지정하기로 하였음.
이렇게 서비스를 추가하면된다.
플레이어가 감지되면 따라가게 만드려면 이렇게 구성할 수 있다.
단 여기서 오른쪽 selector 부분을 관찰자 중단으로 설정해줘야한다.
'Unreal Engine 5 > 강의' 카테고리의 다른 글
[UE5] 데이터 - 게임 데이터 관리하기 (0) | 2024.12.12 |
---|---|
[UE5] 스폰 - 무한 맵 제작 (0) | 2024.12.10 |
[UE5] 아이템 - 여러 종류 아이템 획득하기 (0) | 2024.12.09 |
[UE5] 애니메이션 - 캐릭터 공격 판정 (0) | 2024.12.04 |
[UE5] 애니메이션 - 캐릭터 애니메이션과 몽타주를 이용한 연속 공격 (0) | 2024.12.03 |