Both Gameplay Tasks and AITasks support running “child tasks”.

There are two things you need to handle for this:

  1. Spawn the child task with the correct task owner
  2. Handle the child task’s lifecycle inside the parent

1. Spawning the child task

To correctly set up the child task, we need to use a specific overload of the NewTask or NewAITask functions.

They both have an overload which takes an IGameplayTaskOwnerInterface as one of its parameters. You should use this to spawn the child task.

For example, with an AI task:

MyChildTask = NewAITask<UAITask_SomeTask>(*OwnerController, *this, EAITaskPriority::High, TEXT("Task Name"));
MyChildTask->ReadyForActivation();

Why this specific overload is needed?

Normally the usage pattern for tasks is to create a static constructor function. This is what gets called when the task is being used in blueprints as well. However, for the parent task to manage the child task’s lifecycle, the parent task must be set as the task owner. The other overloads and the static functions don’t allow us to do this.

2. Handling the child lifecycle

Once we have the child task spawned correctly, the functions from IGameplayTaskOwnerInterface can be used in our task class to handle what happens with the child.

For example,

void UMyParentTask::OnGameplayTaskDeactivated(UGameplayTask& Task)
{
	//Check if the task that got deactivated was the child we spawned
	if(&Task == MyChildTask)
	{
		if(Task->IsFinished())
		{
			//The child task finished, do something else
		}
	}
 
	Super::OnGameplayTaskDeactivated(Task);
}

It’s also important to ensure we handle the child task’s cancelation correctly if the parent task ends before the child has finished:

void UMyParentTask::OnDestroy(bool bInOwnerFinished)
{
	if(MyChildTask)
	{
		auto* Task = MyChildTask;
		MyChildTask = nullptr;
		Task->ExternalCancel();
	}
 
	Super::OnDestroy(bInOwnerFinished);
}

If we don’t do the above when destroying the parent, the OnGameplayTaskDeactivated function will think the child task finished, and possibly try to execute some code. Since the parent task is being destroyed, we probably don’t want that to happen. By setting MyChildTask to nullptr in OnDestroy, we ensure no additional logic is triggered when the child task exits.

Other

A good example inside existing engine code for this is UAITask_UseGameplayBehaviorSmartObject. It triggers a UAITask_MoveTo as its child task before running additional logic. I recommend checking its source code for additional examples.