State Tree Event Transition order

Hi, we’re having an issue with the way State Tree Event Transitions are ordered.

When two events are sent to a StateTreeComponent on the same frame, which each trigger a transition on different states, by default the most-derived (most childish?) state transition will be applied, and the transition for the parent state will be ignored.

For instance, in a state tree like this:

Where A has an Event Transition like this:

And A_A has one like this:

And we send the events like this:

…then the event with `Test.StateTree.Tag` will be ignored, even though it was sent before the event with `Test.StateTree.Tag2`.

This is a problem for us because in our project, state tree events can be sent at any time by overlapping systems, and to have an event ignored causes surprising bugs.

I know that there is a “Priority” feature on transitions which can spot-fix some cases of this, but given the wide variety of events and timing, that’s not a scalable solution. What we need is for events transitions to be processed in order of event arrival.

I also personally find it counterintuitive that by default the child state transition has higher priority than the parent state transition. That’s the opposite of what we expect in the StateTrees that we’ve built so far, and we’ve been working around this issue by making parent state transitions all have higher priority than their child states’ transitions.

So my questions:

Is there any way to change this behavior? Is there a way to make all event transitions be processed, even for multiple events arriving on the same frame?

Or, failing that, is there a way to reverse the default priority, allowing parent state transitions to have priority over their child states transitions?

Steps to Reproduce
Set the Rewind Debugger to start when you start PIE, then play the level Main.

Observe that the State Tree in `BP_TestSTEventPrio` ends up in the state Root -> A -> A_B.

Our expectation is that the State Tree ends up in the state Root -> B.

More detail in the description.

So a couple points to consider on this. First, StateTree normally evaluates transitions from the leaf state going back to the root with leaf state transitions taking precedence over an intermediate state. The idea is that the leaf can have the most control over how it should or when it should transition, and this allows a leaf state to “override” a transition event. The second is that the priority system is built for the exact reason of allowing a higher level state’s transition as higher in priority. This system also comes with the caveat that if two transitions are marked as Higher priority, the leaf-most state’s transition is the one that will be used.

This case comes down to how we structured StateTree to transition from leaf nodes first when evaluating. I understand the idea of thinking the event queue would be FIFO since it is called a queue. The name is a bit of a misnomer as events are still handled with the same StateTree logic. This was our effort to keep transitions consistent between events, tick, delegates, etc. where the leaf state is the first transition evaluated and heading back towards the root.

If you want to change the default logic for your project, I caution this as it will change ALL the transition priority to be root most to leaf most. This could break how your designers are currently setting non-event based transitions. You can make a source change for when the execution context builds the TransitionHandlers array to reverse the ordering for transitions. This is a big undertaking that could have other issues such as not allowing for a leaf node to set overrides for transitions that also exist on intermediate states. It could still be handled with priority, but it encounters the same issue of being prone to break due to an overlooked priority setting.

-James

There is no planned work to change how event transitions are handled. Nearly everyone has their own expectation of how transitions and selection should occur, and this is our best effort to address at least most of them. How we see multiple events happening in a single frame is that they all happen at the same time. This allows for designers to dictate which should get the priority or allow the tree to have its default behavior of taking leaf-most transitions first. This idea of same time instead of first also comes to wanting to support multithreading and the idea of a “first” becomes a race condition in StateTree.

In your custom component, are you attempting to trigger the transition when each event arrives? So there could be several calls for trigger transitions each frame assuming multiple events are received that frame? How are you planning to handle the additional events arriving after you have taken the transition? What if one of them matches a transition rule in the new state hierarchy? Are you wanting to transition multiple times? We support multiple transitions in a frame, but there is a pseudo-limit of 5 that could be increased with an engine change.

-James

You are correct that there is not a way to do that in the current implementation of following each event transition.

If you take the first event to transition, would it make sense to possibly transition from that new state again based on an event received while in the old state? If you have lots of logic that runs on enter/exit state, it may make sense.

In your implementation is there any idea of priority on the transitions still or is it running solely on first event received? I could see something like an event for a found SmartObject to be used arriving when a damage event from a player is also received. Design would usually be to give priority to the damage event for transitioning, but you could end up moving to use a smart object and having the overhead of claiming then releasing/invalidating the slot to immediately transition to the damage. This could be even worse if the agent is at the SO already and has an entrance/exit montage that needs played before responding to the damage threat.

-James

To your first question, we want to prevent any transitions being triggered by an old state that is no longer active. After a transition is applied, any subsequent events should be handled by whatever the new active state is.

To your second, the priority is still used for transitions that are triggered by the same exact event*. For separate events, priority is irrelevant in our setup.

Your Smart Object/Damage example is a good one: the issue you rightly raise with a SO entrance montage is just as much a problem with the priority rules enabled, because that case can also occur with a 1-frame delay between the SO Start event and Damage event. So in practice it still needs to be solved.

To visualize it:

In this case, when we get Event.StartUseSmartObject and Event.TakeDamage on the same frame, by default the Smart Object event will “win”, and the damage event will be ignored.

This case can be solved with priority but our issues are

  1. (minor) the default priority is backward IMO, the parent state’s transition should “win”
  2. (major) this doesn’t scale. we can’t judge the priority pairwise between every possible pair of simultaneous events.
  3. (major) the behavior is very different if these events are received on the same frame vs if they are received 1 frame apart.

That’s why we ultimately decided to mod this for our project, so the events are delivered reliably, in order but regardless of timing. I do think that any project which uses large, deep state trees is going to need to make a change like this.

Thanks again for discussing!

* a side note: For state correctness I’d prefer the ability to activate all transitions triggered by a single event, in leaf-to-parent order, making priority irrelevant. But in practice we’re going to avoid single events triggering multiple transitions.

Hi, thanks for your answer.

First, the main issue isn’t the relative priority of transitions caused by the same event (though I do find it backward), but the relative priority of unrelated events. If we send events A and B, a deeper transition handler for A can cause B to be ignored (or vice versa, more unintuitively).

This means that if we send A and B on the same frame we get a different result than if we send A and then 1 frame later send B. Since the exact timing of events is gameplay-dependent, this is a major complication.

So far I’ve been working around this issue by making all root-level transitions `Critical` priority, the second-level ones `High`, and so on, to ensure that even when same-frame events are ignored, the one that is accepted is the more “important” one. I’m not happy with this solution.

I think this is a major limitation to the State Tree system. You can’t get reliable behavior from any state tree with more than one level of hierarchy below root, because any level of hierarchy with transitions at multiple levels may cause events to be ignored depending on the event timing.

I feel that to get reliable behavior the events need to be processed fully in their receipt order, including the transitions. When multiple levels of states transition on the same event, then the transitions should be applied in leaf-to-parent order.

We have tried several engine mods including reversing the transition priority as you suggested (which had the issues you foresaw). Our best approach so far is taking out the priority check in `TriggerTransitions` so that transitions are applied for each event using `RequestTransitionInternal`, potentially multiple transitions per tick.

Currently we’re considering writing our own State Tree component with different logic, which can use `TickTriggerTransitionsInternal` after sending each event to enforce the correct ordering of state transitions. Do you foresee any problems with either of these last two approaches?

Hi again James,

The A and B scenario I described above makes state tree generally unusable for us, since we can’t rely on a system that gives a different result depending on unpredictable gameplay timing.

Is this something that you intend to address in a future version?

Unfortunately, what our designers want is “take all transitions, in the order the events were received”. There doesn’t seem to be a way to do that.

The alternate Send Event function looks like:

void UStateTreeComponent::SendStateTreeEventAndTriggerTransitions(const FStateTreeEvent& Event)
{
	/* removed some validation code here */
 
	InstanceData.GetMutableEventQueue().SendEvent(GetOwner(), Tag, Payload, Origin);
	
	FStateTreeExecutionContext Context(GetOwner(), StateTreeRef.GetStateTree(), InstanceData);
	if (SetContextRequirements(Context))
	{
		const EStateTreeRunStatus PreviousRunStatus = Context.GetStateTreeRunStatus();
		const EStateTreeRunStatus CurrentRunStatus = Context.TickTriggerTransitions();
		
		ScheduleTickFrame(Context.GetNextScheduledTick());
		
		if (CurrentRunStatus != PreviousRunStatus)
		{
			OnStateTreeRunStatusChanged.Broadcast(CurrentRunStatus);
		}
	}
}

If you have any feedback about this approach, please do let me know.

If multiple events are received in a frame, multiple transitions potentially occur. We do want later events to match against the new state hierarchy produced by earlier events.

So it sounds like we’re going to need to modify this to work for a general HFSM use case. Thank you for discussing it with me!