The Games DevProgramming and Game Development. Tips, Tricks and Tutorials.

All about BTTasks in C++

· Updated Artificial Intelligence

Updated for UE 5.8: bNotifyTick and bNotifyTaskFinished are false by default (with an INIT_TASK_NODE_NOTIFY_FLAGS() macro to set them for you), node memory must be constructed via InitializeMemory if your struct has non-trivial members, and the code samples now return after FinishLatentTask and handle aborting.

I get a lot of questions on Unreal Slackers Discord about custom BTTasks in C++, how to create them and some of the more obscure stuff with tasks. I will be going over some of the things that will help you with these tasks.

Constructor setup

First off, let’s start with some constructor stuff we can do. There are a couple of booleans we can set that will fire off different things, and one important one that is false by default.

bNotifyTick = true;
bNotifyTaskFinished = true;
bCreateNodeInstance = false;
NodeName = "My Special Task";

bNotifyTick will have the Task’s TickTask function called. bNotifyTaskFinished will have the task’s OnTaskFinished function called. Both are false by default in UBTTaskNode, so a TickTask override does nothing until you set the flag. Alternatively, call INIT_TASK_NODE_NOTIFY_FLAGS(); in your constructor, which sets each flag to true only if you actually override that function.

The most important one here is bCreateNodeInstance. I will explain this a bit later on, but remember this is false by default, meaning this task is CDO only and can NOT hold a state.

Blackboard key filtering

Another thing you can do in the constructor is set up your BlackboardKey filtering. I have shown some examples below on this:

MyVectorKey.AddVectorFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyVectorKey));

MyObjectKey.AddObjectFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyObjectKey), AActor::StaticClass());

MySpecialActorClassKey.AddClassFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MySpecialActorClassKey), AMySpecialActor::StaticClass());

MyEnumKey.AddEnumFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyEnumKey), StaticEnum<EMyEnum>());

MyNativeEnumKey.AddNativeEnumFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyNativeEnumKey), "EMyEnum");

MyIntKey.AddIntFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyIntKey));

MyFloatKey.AddFloatFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyFloatKey));

MyBoolKey.AddBoolFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyBoolKey));

MyRotatorKey.AddRotatorFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyRotatorKey));

MyStringKey.AddStringFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyStringKey));

MyNameKey.AddNameFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyNameKey));

MyStructKey.AddStructFilter<FMyStruct>(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyStructKey));

As you can see above there are a lot of filter types for the blackboard keys. The benefit to these is they limit those keys to that specific type. A key can have multiple Filters applied (i.e., a Target key could have both ObjectFilter and VectorFilter), though this only makes sense for certain types. A blackboard key is simply a struct of the type FBlackboardKeySelector:

UPROPERTY(EditAnywhere, Category = Blackboard) 
FBlackboardKeySelector MyBlackboardKey;

One of the most important things to do if you have BlackboardKeySelectors is to resolve them, this is done via the InitializeFromAsset override.

void UMyBTTask::InitializeFromAsset(UBehaviorTree& Asset)
{
	Super::InitializeFromAsset(Asset);

	if (const UBlackboardData* BBAsset = GetBlackboardAsset())
	{
		MySpecialKey.ResolveSelectedKey(*BBAsset);
	}
}

Without this the selector has no key ID and reading it at runtime will not find your key.

ExecuteTask

Now the main purpose of a task is to run some logic, so to do that we need to override the ExecuteTask function. This will return our current task status. There are 4 possible statuses: Succeeded, Failed, Aborted and InProgress (Aborted is really meant for AbortTask, ExecuteTask normally returns one of the other three).

EBTNodeResult::Type UMyBTTask::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    if (!OwnerComp.GetAIOwner())
    {
        return EBTNodeResult::Failed;    
    }

    //Do logic here

    return EBTNodeResult::Succeeded;
}

Now this is a very simple task: it will only return Failed if we have no owner, but will return Succeeded when finished. As shown above, this creates a simple task that executes and returns in a single frame.

But what if you want to wait for a delegate callback? Or need to do some stuff on a tick before returning succeeded? Well, we continue this now. First things first, remember BTTasks in C++ do not hold an instance by default. If you need to listen to delegate callbacks, you MUST create the node instanced (bCreateNodeInstance = true). If you do not need to listen to delegate callbacks, you can leave it non instanced, and make use of the Memory feature of BTNodes. Instanced nodes are created per behavior tree component (so per AI using the tree), which costs more memory but lets you use normal member variables.

Non Instanced Node - Memory

Now if you make a node non instanced, and your node does not return Succeeded in ExecuteTask, but rather InProgress, you may need to keep some data for that particular run of the node. As this node is non-instanced, you cannot just store this in the node itself (it is shared between every AI running the tree, so treat the node as const while it runs). Instead all BTNode’s functions pass in a NodeMemory uint8 pointer, which is a per-AI block of memory for this node. We can utilize this rather nicely. Here is a real-world example of a task I made for monster firing.

struct FBTMonsterFireWeaponMemory
{
	float TimeToPauseFireFor = 0.f;
	float TimeToFireFor = 0.f;
	
	float TimeStartedFire = 0.f;
	float TimePausedFire = 0.f;

	bool bFiring = false;
	bool bPausedFiring = false;
	bool bHasStartedFire = false;

	float HalfAngle = 30.f;
};

I created a struct, which we can cast the NodeMemory to. But we need to tell the BTNode that the NodeMemory is the size of our newly created struct. This is achieved by overriding GetInstanceMemorySize. The engine allocates the block zeroed, but does not run your struct’s constructor, so the default member initializers above would not apply on their own. Override InitializeMemory (and CleanupMemory for non-trivial members) and use the InitializeNodeMemory helper, which placement-news the struct for you.

uint16 UBTTask_MonsterFireWeapon::GetInstanceMemorySize() const
{
    return sizeof(FBTMonsterFireWeaponMemory);
}

void UBTTask_MonsterFireWeapon::InitializeMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryInit::Type InitType) const
{
    InitializeNodeMemory<FBTMonsterFireWeaponMemory>(NodeMemory, InitType);
}

void UBTTask_MonsterFireWeapon::CleanupMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryClear::Type CleanupType) const
{
    CleanupNodeMemory<FBTMonsterFireWeaponMemory>(NodeMemory, CleanupType);
}

This is the same pattern the engine’s own tasks (e.g. UBTTask_MoveTo) use. Note this memory is set up when the tree instance starts, not every time the task executes, so reset anything you need fresh in ExecuteTask (which my example below does).

Now we can use our new NodeMemory. Below is a real-world example of this:

EBTNodeResult::Type UBTTask_MonsterFireWeapon::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    AAIController* AIOwner = OwnerComp.GetAIOwner();
    if (!AIOwner)
    {
        return EBTNodeResult::Failed;    
    }

    FBTMonsterFireWeaponMemory* MyMemory = CastInstanceNodeMemory<FBTMonsterFireWeaponMemory>(NodeMemory);
    *MyMemory = FBTMonsterFireWeaponMemory();
    MyMemory->TimeToFireFor = TimeToFireFor;
    MyMemory->TimeToPauseFireFor = TimeToPauseFireFor;
    MyMemory->HalfAngle = HalfAngle;
    
    if (IsInCone(OwnerComp, HalfAngle))
    {
        AMonsterCharacterBase* Monster = AIOwner->GetPawn<AMonsterCharacterBase>();
        if (!Monster)
        {
            return EBTNodeResult::Failed;
        }

        Monster->SetFireEnabled(true);
        MyMemory->bFiring = true;
        MyMemory->TimeStartedFire = Monster->GetWorld()->GetTimeSeconds();
        MyMemory->bHasStartedFire = true;
    }
    
    return EBTNodeResult::InProgress;
}

So what we do here is use CastInstanceNodeMemory to turn the uint8 NodeMemory pointer into our custom struct (it also checks the size against GetInstanceMemorySize in debug builds, which is nicer than a raw reinterpret_cast). We can then start populating that memory space with our data. At the end, we set the task to InProgress, as this task runs for as long as the monster is firing. Now this task is a ticking task, and I do stuff on tick whilst the task is active. This is why the NodeMemory is very important. Here is my TickTask:

void UBTTask_MonsterFireWeapon::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    AAIController* AIOwner = OwnerComp.GetAIOwner();
    if (!AIOwner)
    {
        FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
        return;
    }
    
    FBTMonsterFireWeaponMemory* MyMemory = CastInstanceNodeMemory<FBTMonsterFireWeaponMemory>(NodeMemory);

    //We haven't started firing yet, check to make sure we have LOS and in cone.
    if (!MyMemory->bHasStartedFire)
    {
        CheckForFiringStart(OwnerComp, MyMemory);
        return;
    }

    AMonsterCharacterBase* Monster = AIOwner->GetPawn<AMonsterCharacterBase>();
    if (!Monster)
    {
        FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
        return;
    }

    //No ammo left to fire, end the task
    if (Monster->WeaponComponent->GetActiveWeaponCurrentAmmo() <= 0)
    {
        Monster->SetFireEnabled(false);
        FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
        return;
    }

    if (!HasLOS(OwnerComp))
    {
        Monster->SetFireEnabled(false);
        MyMemory->bHasStartedFire = false;
        FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
        return;
    }

    const float TimeSeconds = AIOwner->GetWorld()->GetTimeSeconds();
    
    const float TimeForStoppingFire = TimeSeconds - MyMemory->TimeStartedFire;
    if (MyMemory->bFiring && TimeForStoppingFire >= MyMemory->TimeToFireFor)
    {
        Monster->SetFireEnabled(false);
        MyMemory->TimePausedFire = TimeSeconds;
        MyMemory->bFiring = false;
        MyMemory->bPausedFiring = true;
    }
    else
    {
        const float TimeForPausingFire = TimeSeconds - MyMemory->TimePausedFire;
        if (MyMemory->bPausedFiring && TimeForPausingFire >= MyMemory->TimeToPauseFireFor)
        {
            Monster->SetFireEnabled(true);
            MyMemory->TimeStartedFire = TimeSeconds;
            MyMemory->bFiring = true;
            MyMemory->bPausedFiring = false;
        }
    }
}

As you can see, I simply cast the NodeMemory again to our custom struct, and I can read the stuff I want, and update the stuff inside the memory. Note the return after each FinishLatentTask, the original version of this sample kept running the rest of the tick after finishing, which is a bug. One other important thing I do is, when the Task needs to finish, I call

    FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);

This tells the tree we are done, and this is the final result, this could also be Failed, but the most important thing is you do call FinishLatentTask if you returned InProgress in ExecuteTask, otherwise your task will never end!

Aborting

If a decorator or a higher priority branch interrupts your task, the tree calls AbortTask. The default implementation just returns Aborted, but if your task has something to clean up (like my monster still firing) override it:

EBTNodeResult::Type UBTTask_MonsterFireWeapon::AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    if (AAIController* AIOwner = OwnerComp.GetAIOwner())
    {
        if (AMonsterCharacterBase* Monster = AIOwner->GetPawn<AMonsterCharacterBase>())
        {
            Monster->SetFireEnabled(false);
        }
    }
    return EBTNodeResult::Aborted;
}

If you can’t finish aborting immediately, return InProgress and call FinishLatentAbort(OwnerComp) later. If bNotifyTaskFinished is set, OnTaskFinished is also called when the task ends, which is another good place for cleanup.

I hope this gives some useful and insightful information on BTTasks. I will cover the other two BT Nodes, Decorator and Service, soon. Though these are similar, they do have a couple of minor differences.

You can always find me on Unreal Slackers or my own private discord. Check the Contact page.