공부중
[UE] BehaviorTree에서 Random으로 Select 해주는 노드 만들어보기(Random Selector) 본문
[UE] BehaviorTree에서 Random으로 Select 해주는 노드 만들어보기(Random Selector)
곤란 2025. 4. 16. 16:57위 내용을 읽고 제가 이해 하도록 정리 하는 글이므로 원글을 바로 보실분은 위 링크로 바로 이동하시면 됩니다.
언리얼 엔진의 BehaviorTree에서 Composites 에는 기본으로 아래와 같이 3개의 Selector가 있다.

Selector
왼쪽에서 오른쪽 순서로 자손을 실행.
자손중 하나가 성공하면 실행 중단.
자손이 하나라도 성공하면 Selector도 성공
자손이 모두 실패시 Selector도 실패

Sequence
왼쪽에서 오른쪽 순서로 자손을 실행
자손중 하나가 실패하면 실행 중단
자손이 하나라도 실패하면 Sequence도 실패
자손이 모두 성공해야 Sequence도 성공한다.

Simple Parallel
메인 테스크(Task) 노드 하나를 전체 트리와 함께 실행.
메인 테스크가 완료 되면 모드 완료 세팅에 따라서 보조 트리를 중단하고 즉시 완료 되거나 아니면 보조트리가 완료될 때까지 대기.
왼쪽 보라색이 메인테스크, 오른쪽이 보조트리이다.
상세 내용은 위 문서를 참고 하면 된다.
그런데 여기에 추가 할 수 있는 기본 제공 데코레이터(Decorator)나 기본 제공 서비스(Service)중에는 랜덤으로 선택할 수 있는 혹은 랜덤으로 무언갈 할 수 있는 것들이 없어서 만들어야 한다.

예를 들어서 Attack1과 Attack2, Smash1과 Smash2가 랜덤하게 선택되어 Task가 실행되었으면 하고 싶어서 이런 글까지 쓰게 되었다.
위 스크린 샷 대로라면 4가지중 하나만 성공시 상위 컴포짓이 성공하는것은 Selector 이므로 이것과 비슷하게 꾸며보면 되지 않을까 싶다.
먼저 Selector는 어떻게 돌아가는지 코드를 보자
int32 UBTComposite_Selector::GetNextChildHandler(FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const
{
// success = quit
int32 NextChildIdx = BTSpecialChild::ReturnToParent;
if (PrevChild == BTSpecialChild::NotInitialized)
{
// newly activated: start from first
NextChildIdx = 0;
}
else if (LastResult == EBTNodeResult::Failed && (PrevChild + 1) < GetChildrenNum())
{
// failed = choose next child
NextChildIdx = PrevChild + 1;
}
return NextChildIdx;
}
자식의 인덱스를 return해주는 식으로 돌아가고
맨 먼저 첫번째(0) 부터 돌아가면서 이전 결과가 실패면 다음(+1) 이런식으로 넘어가며 리턴해주고 있다.
모든 자식을 다 돌고 나서도 실패 했다면 처음에 넣은 BTSpecialChild::ReturnToParent가 return 된다.
이제 이 부분을 적절하게 재구성해서 랜덤한 값으로 return해주면 내가 원하는 Selector가 만들어 질것이다.
이 부분에 대해서는 어떤 형님이 튜토리얼로 만들어 두셨다.
위 링크를 들어가면 코드에 주석까지 친절하게 적어두셨다.
이 내용을 한번 보면서 내가 이해를 해보려고한다.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTCompositeNode.h"
#include "BTComp_SelectorRandom.generated.h"
UCLASS()
class ENEMIES_API UBTComp_SelectorRandom : public UBTCompositeNode
{
GENERATED_BODY()
public:
UBTComp_SelectorRandom(const FObjectInitializer& ObjectInitializer);
virtual void InitializeMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryInit::Type InitType) const override;
virtual void CleanupMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryClear::Type CleanupType) const override;
virtual int32 GetNextChildHandler(struct FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const override;
private:
mutable TArray<int32> ExecutedChildren;
mutable int32 LastSuccessfulChildIdx;
};
InitializeMemory와 CleanupMemory는 각각 InitializeInSubtree와 CleanupInSubtree에서 호출된다.
상위 클래스인 UBTNode까지 올라가도 "// empty in base" 으로 되어있어 적절하게 이곳에서 메모리를 초기화/정리 해주면 될 것 같다.
void UBTComp_SelectorRandom::InitializeMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryInit::Type InitType) const
{
InitializeNodeMemory<FBTCompositeMemory>(NodeMemory, InitType);
ExecutedChildren.Empty();
LastSuccessfulChildIdx = INDEX_NONE;
}
void UBTComp_SelectorRandom::CleanupMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryClear::Type CleanupType) const
{
CleanupNodeMemory<FBTCompositeMemory>(NodeMemory, CleanupType);
ExecutedChildren.Empty();
LastSuccessfulChildIdx = INDEX_NONE;
}
일단 메모리 부분부터 보면 각각
InitializeNodeMemory<FBTCompositeMemory>(NodeMemory, InitType);
CleanupNodeMemory<FBTCompositeMemory>(NodeMemory, CleanupType);
를 작성했는데 이것은 상속받은 UBTCompositeNode에서 각각 저렇게 작성되어 있어서 저렇게 작성한것으로 보인다.
그리고 TArray로 선언한 ExecuteChildren과 LastSuccessfulChildIdx를 비워주고 있다.
그리고 아래 코드에는 주석으로 직접 내용을 적어놓았다.
int32 UBTComp_SelectorRandom::GetNextChildHandler(FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const
{
int32 NextChildIdx = BTSpecialChild::ReturnToParent;
if (PrevChild == BTSpecialChild::NotInitialized)
{
// First execution, choose a random child
// 첫 번째 실행 시, 임의의 자식을 선택합니다.
NextChildIdx = FMath::RandRange(0, GetChildrenNum() - 1);
if (NextChildIdx == LastSuccessfulChildIdx && GetChildrenNum() > 1)
{
// Avoid the last successful child being chosen first if possible
// 가능하면 마지막으로 성공한 자식이 먼저 선택되지 않도록 합니다.
NextChildIdx = (NextChildIdx + 1) % GetChildrenNum();
}
}
else
{
// Handle last result
// 마지막 결과를 처리합니다.
if (LastResult == EBTNodeResult::Succeeded)
{
// If last child succeeded, return success and reset
// 만약 마지막 자식이 성공하면 성공을 반환하고 재설정합니다.
LastSuccessfulChildIdx = PrevChild;
ExecutedChildren.Empty();
return BTSpecialChild::ReturnToParent;
}
else if (LastResult == EBTNodeResult::Failed)
{
// If last child failed, mark it as executed
// 마지막 자식이 실패하면 실행됨으로 표시합니다.
ExecutedChildren.Add(PrevChild);
// If all children have failed, return failure
// 모든 자식이 실패하면 실패를 반환합니다.
if (ExecutedChildren.Num() >= GetChildrenNum())
{
ExecutedChildren.Empty();
LastSuccessfulChildIdx = INDEX_NONE;
return BTSpecialChild::ReturnToParent;
}
// Choose a new random child that has not been executed yet
// 아직 실행되지 않은 새로운 임의의 자식을 선택합니다.
TArray<int32> AvailableChildren;
for (int32 ChildIdx = 0; ChildIdx < GetChildrenNum(); ++ChildIdx)
{
if (!ExecutedChildren.Contains(ChildIdx))
{
AvailableChildren.Add(ChildIdx);
}
}
// Choose a random child index from available children
// 사용 가능한 자식에서 임의의 자식 인덱스를 선택합니다.
NextChildIdx = AvailableChildren[FMath::RandRange(0, AvailableChildren.Num() - 1)];
}
}
return NextChildIdx;
}
그냥 아주 간단하게 Random함수를 이용해서 값을 return해주면 되는것이 아니냐? 라고 생각 할 수 있지만
그렇게되어도 실행은 되지만 만일 선택받은 자식이 실패한 경우 해당 Selector의 자식에 있는 다른 Task가 실행되지 않고 실패처리 되므로 주의해야 한다.
'Programing > UnrealEngine' 카테고리의 다른 글
| [UE] Editor에서 SeamlessTravel이 안되는 문제. (0) | 2025.03.09 |
|---|---|
| [UE] WAV파일을 DownSampling, Stereo To Mono로 변경 해보자. (0) | 2024.07.29 |
| [UE] 마이크로 전달한 음성 데이터를 wav파일로 저장해보자. - 3 (0) | 2024.07.16 |
| [UE] 마이크로 전달한 음성 데이터를 wav파일로 저장해보자. - 2 (3) | 2024.07.16 |
| [UE] 마이크로 전달한 음성 데이터를 wav파일로 저장해보자. - 1 (4) | 2024.07.15 |