Title: NPC AI StateTree logic: Guard not responding or attacking empty air (GAS + C++ Architecture)

Title: NPC AI StateTree logic: Guard not responding or attacking empty air (GAS + C++ Architecture)

Hi everyone,

I’m working on a Guard AI system in Unreal Engine 5.8 using StateTrees, AI Perception, and the Gameplay Ability System (GAS). I’ve successfully set up the foundation in C++, but I’m hitting some logic traps in the StateTree behavior (Pictures below might help if needed).

The Current Setup:

  • Base Class: C++ AMyCharacterBase
  • (implements IAbilitySystemInterface)
  • Perception: BaseGuardAIController using Hearing and Sight.
  • StateTree workflow: Idle → MoveToNoise → Inspect → Attack → ReturnToPost.
  • Combat: Custom StateTree Task
  • BTT_ActivateAbility that triggers a GAS GA_MeleeAttack.

The Problem:

  1. The Guard reaches the noise location just fine, but it just stands there after it has reached the location, not attacking as it should. It should be attacking after it reaches the location, even if the player isn’t there (attacking the air).
  2. The transitions between “Inspecting” and “Combat” don’t seem to trigger reliably when the player is actually seen or when it reaches the location.
  3. I’m struggling with the logic flow to make the Guard return to his starting position only after the target is dead or lost.

What I’ve Tried:

  • Verified NavMesh is active.
  • Checked PossessedByin C++ to ensure StartLogic() is called.
  • Increased AcceptableRadius to 50.0

I’ve attached screenshots of my StateTree graph and parameters. Any advice on how to better structure these transitions or handle the TargetActor Logic would be greatly appreciate!

MoveTo finishing and the guard just standing there means the move succeeded and the next transition’s condition is false. Acceptable Radius and the navmesh are already out of it. “Swing at the noise even if nobody is there” and “go home only after the target is dead or lost” are two different edges, and they are probably both gated on TargetActor != null, so neither fires reliably.

Split it:

  • MoveToNoise success (the task’s Succeeded output, not “I can see a pawn”) should go to Inspect or straight to an Attack state. Do not require a perceived actor on that edge. Hearing stimuli expire, so a condition that reads the live perception target goes null right when the move finishes, which is the frame you needed it.
  • Latch the target on the controller. When sight or hearing fires, copy that actor into your own LatchedTarget, and only clear it from an explicit lost-target timer task. Bind the StateTree conditions to that pointer, not to the raw stimulus. Otherwise Inspect to Combat flickers off every tick the stimulus is stale, and the tree never stays in Attack.
  • Attack has to be enterable with LatchedTarget null if you want a swing at empty air. Inside BTT_ActivateAbility, if TryActivateAbility returns false, fail the state. Do not succeed it and wait. A melee ability that requires target data fails immediately on a null actor, which looks exactly like “he arrived and stood there.” Pass the inspect location, or a target-data that does not need a pawn, and log the activate result. Also confirm the ability is actually granted and not blocked by tags.
  • ReturnToPost is a transition out of Attack or Inspect: target is dead, or the latched target was cleared and the lost timer finished, then MoveTo home, success back to Idle. Do not leave Return as a sibling state that is true at the same time as Attack. The higher-priority one will cancel the move home, or a flickering false will abort it.

Run it once in the StateTree debugger and look at which condition failed on the tick MoveTo succeeded. That edge is the bug. The GAS task is the second place to log, not the navmesh.

Reaching the noise point and then just standing there is a transition problem, not NavMesh. Move To finishing does not attack anything. Combat only runs if a transition into that state becomes true, and “attack the air” will never happen if Attack’s enter condition requires a perceived actor. Hearing gives you a location, not a target.

Split the data. Keep NoiseLocation (vector) and bHeardNoise separate from TargetActor. Move To should use the vector. Do not AND that transition with “Target Actor is valid”.

Wire the tree like this:

  1. Move To → Attack on success, with no target check. On the Move To task, finish the task when the move succeeds. The transition out should be State Succeeded / Completed, condition empty (or only “move succeeded”). That is the swing-at-empty-air path. Put a second, higher-priority transition from Move To (and from Inspect) that also goes to Attack when sight actually has an actor, so a visible player does not wait for the inspect to finish.

  2. Perception does not write StateTree parameters by itself. In the controller, on Target Perception Updated: if the sense is Sight and it was successfully sensed, set TargetActor; if it is Hearing, set NoiseLocation and bHeardNoise. Those have to be StateTree parameters (or bound from the controller context), not only blackboard keys. If you only set the blackboard, the tree never sees them.

  3. Return home only after the swing is really over. Do not leave Attack on “task completed” if BTT_ActivateAbility completes as soon as TryActivateAbility returns. That pops the state the same frame and can cancel the ability. Have the task stay running until the ability’s ended delegate (or a gameplay event), then transition on “ability ended AND (target dead OR sight lost for N seconds)”. Do not put that “lost target” transition on the whole tree, or it will cancel the move to the noise.

  4. A silent TryActivateAbility failure looks exactly like standing still. Print the failure. The usual causes: the ability was granted on the controller’s ASC instead of the pawn’s (or never granted in PossessedBy), it is blocked by an activation tag, or the state exited and cancelled it. Grant GA_MeleeAttack on the pawn ASC and only complete the StateTree task from the ability-ended callback.

Priority order on Move To: “saw the player” above “arrived”, and “go home” only from Attack / Inspect. If the guard still stands on arrival, the Succeeded transition is missing or it still requires a valid target.