Execute Function Behavior Tree Task

AI Aug 26, 2019

Tldr: I created a behavior tree task that lets you execute a function of your choosing on the target blackboard key object. Code snippets at the bottom, relevant files here on GitHub.

Hey everyone! Yesterday I was working on some AI in Unreal Engine and I found myself often having to make a task just to do a simple function call in the AI-controlled pawn, which is rather pointless if all you do in that task is execute a function and finish execution. So I decided to make a single task that does this for me.

Final result

General behavior of the node:
You create the node, select the target class you want to target with this node, and from a dropdown you can select all available UFunctions of this class. I personally filter UFunctions that have a return val or params out to more easily find the function I want to execute.

Technical behavior of the node:
I created a new struct FFucntionContext that takes a TSubClassOf<UObject> and an FString, the TSubClassOf<UObject> is used to find the class we want to target and the Fstring is to store the function to execute.

In an editor plugin, I created a IPropertyTypeCustomization for this struct called FFunctionContextCustomization, for the TSubClassOf<UObject> I just generate the value widget as I don’t need to modify that. But for the string, I create a dropdown that has as OptionsSource the available and filtered UFunctions. We can do this by getting the value of the property as a formatted string and find the UClass. From the UClass we can iterate over all UFunctions this class has and get their name and flags, and other useful information to determine whether or not this UFunction is relevant for me.

Full code can be found at: https://github.com/CelPlays/SaltyAI

Customizing the Property

To get a custom dropdown for the property you need to create an IPropertyTypeCustomization class for your struct and override CustomizeHeader. Once you did that you have full control over the Slate visuals and behavior of your struct property.

For the TSubClassOf property, I create the default property widget in the ValueContent of the HeaderRow and for the string property, I create a dropdown which gets filled when you select a class.

I also bind to SetOnPropertyValueChanged so that when the class changes the actual array for the options will update and you can reselect your class.

void FFunctionContextCustomization::CustomizeHeader(TSharedRef<class IPropertyHandle> StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
	ClassProperty = StructPropertyHandle->GetChildHandle("ContextClass");
	FunctionProperty = StructPropertyHandle->GetChildHandle("FunctionToExecute");

	FSimpleDelegate OnChange;
	OnChange.BindRaw(this, &FFunctionContextCustomization::OnClassChange);
	ClassProperty->SetOnPropertyValueChanged(OnChange);

	OnClassChange();

	HeaderRow.NameContent()
	[
		StructPropertyHandle->CreatePropertyNameWidget(FText::FromString("Function Context"))
	]
	.ValueContent()
	.MinDesiredWidth(500)
	[
		SNew(SVerticalBox)
		+ SVerticalBox::Slot()
		[
			ClassProperty->CreatePropertyValueWidget()
		]
		+ SVerticalBox::Slot()
		[
			SAssignNew(ComboBox, SComboBox<FComboItemType>)
			.OptionsSource(&Functions)
			.OnSelectionChanged(this, &FFunctionContextCustomization::OnSelectionChanged)
			.OnGenerateWidget(this, &FFunctionContextCustomization::MakeWidgetForOption)
			[
				SNew(STextBlock)
				.Text(this, &FFunctionContextCustomization::GetActiveFunction)
			]
		]
	];

	UpdateActiveSelectedClass();
}

Iterating UFunctions

Here I go over all UFunctions of the ContextClass. I only get functions that are BlueprintCallable, have no return value, does not have a native implementation and has no params. Obviously these rules are on a per-project basis and you can make these whatever you want.

for (TFieldIterator<UFunction> FuncIt(ContextClass); FuncIt; ++FuncIt)
{
	UFunction* Function = *FuncIt;

	//Only blueprint callable
	if (!Function->HasAnyFunctionFlags(FUNC_BlueprintCallable))
	{
		continue;
	}

	//Ignore return val function
	if (Function->GetReturnProperty())
	{
		continue;
	}

	//Ignore native functions
	if (Function->HasAnyFunctionFlags(FUNC_Native))
	{
		continue;
	}

	//If function has params ignore the function
	if (Function->NumParms > 0)
	{
		continue;
	}

	Functions.Add(MakeShareable(new FString(Function->GetName())));
}

Executing the UFunction

You can execute the UFunction by first finding the UFunction and then processing the event. I also added a boolean for bReturnSuccessIfInvalid, which will return success even if the function call wasn’t successful.

UBlackboardComponent& BlackboardComp = *OwnerComp.GetBlackboardComponent();
UObject* Object = BlackboardComp.GetValueAsObject(Target.SelectedKeyName);

if (!Object)
{
	return bReturnSuccessIfInvalid ? EBTNodeResult::Succeeded : EBTNodeResult::Failed;
}

UFunction* Func = Object->FindFunction(FName(*FunctionContext.FunctionToExecute));

if (Func)
{
	Object->ProcessEvent(Func, nullptr);
}
else
{
	return bReturnSuccessIfInvalid ? EBTNodeResult::Succeeded : EBTNodeResult::Failed;
}

return EBTNodeResult::Succeeded;

Prerequisites

To get this to work you also have to register your IPropertyTypeCustomization in StartupModule() of your editor module. You can do this with the following code:

FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");

//Custom properties
PropertyModule.RegisterCustomPropertyTypeLayout("FunctionContext", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFunctionContextCustomization::MakeInstance));

Also, make sure you add PropertyEditor as a dependency in your editor module. For your Runtime Module you want to add AIModule and GameplayTasks as a dependency. You have to add GamepayTasks as a dependency, otherwise it won’t compile.

Tags