Gameplay Tasks running after NPC despawns

Hello, I have a question about the intented use of Gameplay tasks. We are using Mass Representation to spawn and despawn NPCs, and on most despawns several Gameplay Tasks are left running on the NPC, generating errors on every other node - as the NPC is “pending kill”, and it’s unpossesed, any node that accesses the NPC or AI controller throws blueprint errors.

I don’t see a way any task should (or could) work when the NPC is despawned, however I haven’t found any mechanism that kills the tasks. Is every system that starts the task supposed to kill the task? If so, why isn’t there a check that it happened?

I am currently just killing all tasks on unposses, which seems to work fine so far. But is it a reasonable solution? What is the intended way to use this system?

Hello!

We use a similar method to ending GameplayTasks in AIContoller::OnUpossess. I think your approach is reasonable as it mirrors our own very closely. I would remove any tasks from whatever owns the gameplay tasks component when the owner is being removed/unpossessed/destroyed.

I do have a follow up question though. Are you creating AI controllers for your NPC Actor representations? Are these tasks created by using the StateTree components for when hydrating to an actor?

-James

That makes sense. Not all projects using Mass spawn AI controller for their Pawns, but it is perfectly feasible. In your AI controller, do you call the Super::OnUnpossess function? Or is there some logic happening in there you wish to avoid in your, I am assuming, custom controller? We thought the addition to OnUnpossess should handle cleaning up all tasks, but if it is not, it is something we would be curious to investigate.

-James

I see what you mean with the AI controller needing to happen earlier. We had a similar issue with cleaning up the brain component that had to be moved forward in OnUnpossess recently since the StateTreeAIComponent expects the AI controller to be valid when shutting down. I believe your fix is completely reasonable in that regard. I spoke with some of the other team members, and our thinking is that it may need to be moved forward in our base implementation. However, that work will require extensive testing to move as all internal projects will need to be validated it does not break any behavior there.

As for the function saying claimed resources in the name, I believe that may be a misnomer that was changed as the feature has seen iterations. Looking at the code and speaking with others on the team, it should end any tasks associated with the owner that is passed into the function.

That is the only engine-provided means of using StateTree from a Smart Object. I think it is a heavy approach to have the rest of the plugin, which I do not believe is in a functional state due to changes in the contextual animation scene feature, just to use StateTree. I think I would create a gameplay behavior config similar to the UGameplayBehaviorConfig_BehaviorTree config in the GameplayBehaviorSmartObject plugin. Or something similar to the behavior definition in the GameplayInteractions plugin where you can use a StateTree schema other than the GameplayInteraction schema.

-James

Hmmm… I am going to send this over to a colleague from our gameplay team who own the component as there may be some other code to call that I am not aware of.

Your approach for the GameplayInteractions mirror what we would do internally. There may even be a version of it used in internal projects that has not been generalized and added back to the engine.

-James

Hey there, custom GameplayTasks only end up in the TaskPriorityQueue if they return true for RequiresPriorityOrResourceManagement(), so:

  • Only if they list resources they depend on,
  • Or if their class sets bCaresAboutPriority to true

You would also need to call ReadyForActivation() on those tasks to have them enter the priority queue. A quick summary of the priority queue: this feature mainly exists for queueing a list of tasks and if they should be activate and tick in some sorted order: their priority.

The TL;DR of this is that EndAllResourceConsumingTasksOwnedBy() is not relevant for tasks that (1) don’t use resources and (2) don’t make sure of the priority system. Writing your own “End all tasks” function that iterates KnownTasks is perfectly valid.

Another pattern is that some other external owner of the task manages cleaning them up. Gameplay Ability System (GAS) is another user of the GameplayTasksComponent:

  • UAbilitySystemComponent derives from UGameplayTasksComponent
  • UAbilityTask derives from UGameplayTask
  • UGameplayAbility (instance of an ability blueprint) spawns UAbilityTask (ability task nodes in the blueprint)

In GAS, UGameplayAbility manually tracks which tasks it starts in ActiveTasks. When the ability end, it ends its own tasks, see UGameplayAbility::EndAbility():

// Tell all our tasks that we are finished and they should cleanup
		for (int32 TaskIdx = ActiveTasks.Num() - 1; TaskIdx >= 0 && ActiveTasks.Num() > 0; --TaskIdx)
		{
			UGameplayTask* Task = ActiveTasks[TaskIdx];
			if (Task)
			{
				Task->TaskOwnerEnded(); // *** see my notes
 
			}
		}
		ActiveTasks.Reset();	// Empty the array but don't resize memory, since this object is probably going to be destroyed very soon anyways.

*** TaskOwnerEnded calls both Task->OnDestroy() and GameplayTasksComponent->OnGameplayTaskDeactivated().

So to revisit your questions from the first post:

Q: “Is every system that starts the task supposed to kill the task?”

A: GAS does things this way, so a system managing their started tasks is an intended approach. But not tracking them all and just ending them from a total list (whether PriorityTaskQueue or KnownTasks), is fine too.

Q: If so, why isn’t there a check that it happened?

A: We haven’t needed a catch-all “End all tasks” function because GAS handles and hides the responsibility, and there are no other derived classes from GameplayTasksComponent. But if you have a custom one or are using GameplayTasksComponent directly, it sounds useful to have a catch-all “end all tasks”.

If you run into issues based on the order in which tasks are then ended, you can consider using the priority queue after all by letting task classes set bCaresAboutPriority to trueand return a different value for GetPriority(). Just be aware that highest priority gets activated first but also deactivated first.

Yes, we are creating AI controllers for our Actor representations. We are currently aiming to have some AI features enabled only for high resolution actors (such as perception or pathfinding), which is why we use an AI controller.

We have a mass processor that chooses a smart object for the NPC and claims it, but does not interact with it in any way (it only moves to its position). When we hydrate the entity with an actor, this actor then calls UseSmartObjectWithGameplayInteraction, which uses the SO (and creates the task).

We do call Super::OnUnPossess in our controller, however that first does the unpossesion (disconnects controller and pawn) and then terminates the tasks. Many of our tasks have some exit code, which often uses the AI controller in some way, and end up throwing errors all over the place. This would be solvable by checking every access to the AI controller for IsValid, and ignoring these parts of the code, however this increases complexity, so we decided to terminate tasks before calling Super::OnUnPossess, which lets all exit code run normally.

Also not all of the tasks get terminated, AAIController::OnUnPossess calls only EndAllResourceConsumingTasksOwnedBy(this). I am not sure what “resource consuming” tasks are, if those are the same resources Tasks require and claim (Logic, Movement), then we have tasks that consume none of those.

Our main task is usually the one from UseSmartObjectWithGameplayInteraction, which consumes no resources, because if it did, nothing inside it would ever work. This tasks executes a State tree, in which there is, for example, MoveTo task, which requires both Logic and Movement.

Btw. is using UseSmartObjectWithGameplayInteraction the correct way of running State trees from smart objects? You have mentioned before there might be a better way, however all tutorials and talks mention using this plugin, and I haven’t found any other way.

I originally suspected it was a misnomer, as the function doesn’t really check for any resources - however it only ends tasks in TaskPriorityQueue. I don’t fully understand how the UGameplayTasksComponent works will all it’s tasks lists, but in testing I found that our tasks are generally not in this list. In my code I have been terminating all owned tasks from KnownTasks list, which worked fine for me.

As for the plugin, we are slowly moving away from it. We already needed several changes to the task, and ran into some issues in subclassing them (as the task, context and schema are very much dependent on each other), so we already have our own versions, which started as copies (because of the subclassing issues), with some leftover GameplayInteractions dependencies, which we are slowly removing.