I'm new to xstate - I just wanted to know what might be a good way to implement this. I have two possible solutions after the explanation/requirement.
In Unity's mechanim state machine system there's a concept of 'trigger' parameters. 'Trigger' in this context has a different meaning than the SCXML 'trigger'.
The basic idea: there are parameters that live as state within the machine. Almost all parameter types like floats, bools, ints can be used within conditions, just like as a guard in statechart terminology. These parameters are immutable with the exception of trigger parameters. When the transition finishes, the parameter is reset. Example of how to use this:
shouldAttack trigger parameter within that FSMidle state to attack state with a simple condition that uses the trigger parameter shouldAttack.attack state to idle state with no conditions. However an "Exit Time" setting can be specified to automatically invoke the transition after the exit time is reached. The 'shouldAttack' trigger parameter is reset when this transition is invoked.Btw, I looked through the SCXML spec and couldn't find something like this.
Solution 1:
So I want to model this with xstate. Unless I'm missing something a naive way would be to implement it with a condition + side effect.
shouldAttack represented as a boolean: idle: {
on: {
ATTACK: {
attacking: {
cond: (extState, eventObj) => extState.shouldAttack
}
}
}
},
attacking: {
onEntry: ['invokeAttack'],
on: {
IDLE: 'idle'
// ...
}
Solution 2:
I guess a better way to do this would be to implement this in a separate runner/interpreter on top of xstate. Maybe it would look something like this:
idle: {
on: { ATTACK: { attacking: { trigger: true } } }
},
attacking: {
onEntry: ['invokeAttack']
on: { IDLE: { idle: { exitTime: 1000 } } }
}
This mirrors the minimal amount of info you would specify in Mechanim to get this working. Then the interpreter would translate this into a condition + timer and invokeAttack wouldn't have to handle that.
Any other solutions? On first glance it seems like this could live in xstate-interpreter since that's already handling delayed actions/event emitting.
After reading about triggers (e.g. ResetTrigger) I wonder if this is pretty close to what the concept of activities — which isn't part of the SCXML spec, but it is part of the Statechart nomenclature from Harel all those years ago.
In Statecharts, an activity is the notion of a long running thing that is happening _while in a state_ more or less. In contrast, an action is an instantaneous thing that happens when _entering_, _exiting_ states or _transitioning_ between states. The important thing is the duration of time.
In xstate, it's possible to note down which activities should be "on" in any given state; they are inherently off otherwise. Essentially it's a set of named booleans.
The way I understood your code example, you have two states, and when in the "attacking" state you want the "attacking" trigger to be true. This can be done as an activity:
idle: {
on: {
ATTACK: {
attacking: {
cond: (extState, eventObj) => extState.shouldAttack
}
}
}
},
attacking: {
activities: ['invokeAttack'],
on: {
IDLE: 'idle'
// ...
}
The state has a set of booleans called activities, and whenever you transition, you can ask which activies are set, and which are cleared, and it'll even tell you which activities were started/stopped during this last transition.
Activities automatically stop when the state it's in exits, so in the example above the "invokeAttack" activity is set only when in the "attacking" state.
Please forgive me if I've completely misunderstood the use case for triggers!
Also, in 3.2 (will document this), you'll be able to add:
import { actions: { send, stop } } from 'xstate';
// ...
attacking: {
// stop the 'invokeAttack' activity after 2s
onEntry: [send(stop('invokeAttack'), { delay: 2000 })],
activities: ['invokeAttack'],
on: {
IDLE: 'idle'
// ...
}
in order to stop an activity after a delay.
But now I'm thinking that something like this (⚠️ maybe future syntax! ⚠️) might be easier, as syntactic sugar:
import { activity } from 'xstate'; // does not exist yet
// ...
attacking: {
activities: [activity('invokeAttack', { timeout: 2000 })],
on: {
IDLE: 'idle'
// ...
}
☝️ Thoughts on this possible enhancement @mogsie @adiun ?
Okay, I just read through the Activities proposal (#46) and I agree it's pretty similar to triggers. Cool that it's already in xstate 👍
So thinking about how this would work in a more realistic scenario: a user would like to able to change a shouldAttack variable somewhere to true and under the hood, the machine automatically transitions to the attacking state. If the user ever set shouldAttack to false while in the attacking state then the machine would automatically transition back to the idle state. At an even higher level, the user wants to be able to press and hold an 'attack' button. When depressed, no more attacking.
A simple way for the user to do this would be to have shouldAttack be a property with getters/setters. The getter would check whether the invokeAttack (maybe should be called isAttacking?) activity is set. The setter would transition to the ATTACK state if the value is true, etc.
No condition is needed in this scenario, correct?
Also, I like the syntax enhancement a lot. But wanted to be clear about two things:
attacking to idle)?It's important to note that all statecharts and state machines are meant to be _reactive_ in that they don't _do_ anything, they react to events. With this premise it means that "a user changing a shouldAttack variable somewhere to true" _has_ to emit an event that makes its way to the statechart interpreter. That event needs to be named; let's call this requestedAttack. It's interesting to note that you don't really need to retain the boolean shouldAttack anymore, it's fully possible to let the statechart control the ownership of that boolean flag.
The requestedAttack event means "The user asked to attack".
idle: {
on: {
requestedAttack: 'attacking' // the user asked to attack so yeah, go go go!
}
},
This part of the statechart says: If you're idle and the user requested an attack, go to the 'attacking' state. When you define the 'attacking' state, you start defining side-effects of being in the attacking state. For example, an attacking activity:
attacking: {
activities: ['attacking'],
on: {
IDLE: 'idle'
}
}
The statechart will, with these two states and the following transition give you the following output:
state = 'idle'
state = machine.transition(state, 'requestedAttack')
state.activities.attacking; // true
state.actions // array containing instructions to start the `attacking` activity.
You can take state.activites and use that to update UI or whatever, knowing that that flag is now set.
Now that this flag has been set, I understood two ways of it resetting to false. If the user stop the attack, that should be a stopAttack event or something. We handle this in the attacking state:
attacking: {
activities: ['attacking'],
on: {
IDLE: 'idle'.
stopAttack: 'idle'
}
}
When that event is passed to the machine.transition it will clear theactivities.attacking bit on the returned state object.
The second possibility is that you want the attack to finish after a certain amount of time. This is (slightly) outside the scope of xstate, but certainly within the realm of statecharts. You really want to model this as "please exit this state after a certain amount of time" using a delayed transition.
In xstate you have to do this yourself using a delayed event, perhaps calling setTimeout to send the IDLE event after 2 seconds or something.
The crux is that
Hope this helps :)
I wanted also to point out how the IDLE and stopAttack essentially do the same things, right, they take you back to the idle state, calling off the attack. You might be tempted to change things around so that you make the "user stopped pressing attack button" to generate the IDLE event, instead. But the idea is in fact to let the statechart decide what to do, and it needs to know as much as possible about what's happening. A user releasing an attack button is _different_ than e.g. an NPC going out of range, or the player fainting. All of these might lead to the calling off of the attack. The fact that the statechart treats these equally is up to the statechart. You should be forwarding pretty "high level" events to the statechart. A rich set of events allows you to (at some point) decide that you want to react (slightly) differently to those events.
@mogsie that explanation was super helpful!
Re. shouldAttack - I went into too much detail here; I meant that it's just a property somewhere without backing storage/retention; just a wrapping getter/setter that reads/drives the machine through events. Instead of 'transition to ATTACK state' I should have said 'send ATTACK event'.
I now understand that activities are purely side-effects and there's really no notion of manually 'stopping an activity to exit the state', which makes this different from the mechanim 'Trigger' - it's all about sending an event (either on a timer or manually) to transition out of a state, which will clear the activity flag. And sending an event to set the activity flag again.
The 'crux' you wrote was especially helpful. Thanks again!
Closing as the question was answered 😄