All about BTTasks in C++

I get a lot of question on Unreal Slackers Discord about custom BTTasks in C++, how to create 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.

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

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

bNotifyTick will have the Task Tick function called. bNotifyTaskFinished will have the task’s OnTaskFinished function called. 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.

Another thing you can do in the constructor is setup 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>());

MyEnumKey.AddNativeEnumFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyEnumKey), "MyEnum");

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));

As you can see above there is 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 (ie a Target key could have both ObjectFilter and VectorFilter), thos 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 initialize them, this is done via InitializeFromAsset override.

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

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

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.

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, will only return failed if we have no owner, but will return succeeded when finished. This is a very simple BTTask set up, and shown above will create 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. If you do not need to listen to delegate callbacks, you can create a non instanced node, and make use of the Memory feature of BTNodes.

Non Instanced Node – Memory

Now if you make a node non instanced, and your node does not return succeeded in Execute Task, but rather calls InProgress, you may need to keep some data for that paticular node. Now as this node is non-instanced, you can not just store this in the node itself, instead all BTNode’s functions pass in a NodeMemory uint8 pointer. 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 off the size of our newly created struct. This is achieved by overriding GetInstanceMemorySize

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

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

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

    FBTMonsterFireWeaponMemory* MyMemory = reinterpret_cast<FBTMonsterFireWeaponMemory*>(NodeMemory);
    MyMemory->TimeToFireFor = TimeToFireFor;
    MyMemory->TimeToPauseFireFor = TimeToPauseFireFor;
    MyMemory->HalfAngle = HalfAngle;
    
    if (IsInCone(OwnerComp, HalfAngle))
    {
        AMonsterCharacterBase* Monster = OwnerComp.GetAIOwner()->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, we reinterpret cast the uint8 NodeMemory pointer into our custom struct we created. 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 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)
{
    if (!OwnerComp.GetAIOwner())
    {
        return;
    }
    
    FBTMonsterFireWeaponMemory* MyMemory = reinterpret_cast<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 = OwnerComp.GetAIOwner()->GetPawn<AMonsterCharacterBase>();

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

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

    const float TimeSeconds = OwnerComp.GetAIOwner()->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. One other important thing I do is, when the Task needs to finish, I call

    FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);

This tells the task, 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!

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

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