Schema extensions support Finite State Machine (FSM) feature, which automatically changes Component state by condition for you. To define a FSM, first define a enum indicates states.
public enum CharacterState { Normal, Angry, Fever, MAX }
And IKeyComponent
class to store and index the state of the entity.
public struct CharacterStateComponent : IKeyComponent<EnumKey<CharacterState>>
{
public EnumKey<CharacterState> key { get; set; }
public CharacterStateComponent(CharacterState state) => key = state;
}
Since the key is enum
, which does not implement IEquatable<T>
, we use special wrapper EnumKey<T>
. You can use it as it is the inner enum.
Now you can define FSM class, inherit StateMachine<TComponent>
.
public class CharacterFSM : StateMachine<CharacterStateComponent>
{
public interface IRow : IStateMachineRow {}
protected override void OnConfigure()
{
var config = Configure<IRow>();
var stateNormal = config.AddState(CharacterState.Normal);
var stateAngry = config.AddState(CharacterState.Angry);
var stateFever = config.AddState(CharacterState.Fever);
}
}
Same manner as Index
, StateMachine
accepts StateMachineKey
.
Also define Interface Row IRow
to represent the Rows using State Machine. OnConfigure
method is used to configure your State Machine. Call Configure<IRow>
to get a builder for State Machine. By calling AddState
you can add State.
Now you have states of State Machine, but it won't have any effect until you add Transitions between States.
Transition describes how State changes. In OnConfigure
you can add Transition and Conditions.
public class CharacterFSM : StateMachine<CharacterFSMState>
{
public interface IRow : IStateMachineRow,
IEntityRow<RageComponent>,
IEntityRow<TriggerComponent>
{}
protected override void OnConfigure()
{
var config = Configure<IRow>();
var stateNormal = config.AddState(CharacterState.Normal);
var stateAngry = config.AddState(CharacterState.Angry);
var stateFever = config.AddState(CharacterState.Fever);
stateNormal.AddTransition(stateAngry)
.AddCondition((ref RageComponent rage) => rage.value >= 30);
stateAngry.AddTransition(stateNormal)
.AddCondition((ref RageComponent rage) => rage.value < 20);
stateNormal.AddTransition(stateFever)
.AddCondition((ref RageComponent rage) => rage.value < 10)
.AddCondition((ref TriggerComponent trigger) => trigger.value);
}
}
Note that here, IRow must include inner IStateMachineRow
and IEntityRow<T>
s to use in Transitions and Conditions. It is important to manually do this, so you can ensure any Rows using State Machine will have all those Components.
By calling FromState.AddTransition(ToState)
you define a Transition. You also should add Condition for Transition to happen, by calling AddCondition
. Conditions take a lambda with single ref IEntityComponent
parameter and bool
return value.
All Conditions must return true
for the Transition to be executed. If you want another set of Conditions, you can add another Transition with same States. If there are multiple Transition met the Conditions, the Transition added first in OnConfigure()
has higher priority.
You can also use special config.AnyState
property to define Transition from any States.
If you want to set Component values when Transition happens, you can define ExecuteOnEnter
and ExecuteOnExit
Callbacks. Also remember to add IEntityRow<Component>
to your IRow
, for each Component you'll use.
stateSpecial
.ExecuteOnEnter((ref TriggerComponent trigger) => trigger.value = false)
.ExecuteOnEnter((ref SpecialTimerComponent timer) => timer.value = 1)
.ExecuteOnExit((ref RageComponent rage) => rage.value = 5);
Callbacks receive same parameter as Conditions, but without return value.
To use State Machine, first add StateMachine.IRow
to your Row. That means all other components you used for Conditions and Callbacks will automatically included to your Row as well. This also means when spec has changed, you don't have to edit all Entities. You only edit StateMachine.IRow
and it will add all Rows that uses it.
public sealed class CharacterRow : DescriptorRow<CharacterRow>,
IQueryableRow<RageSet>,
CharacterFSM.IRow
{ }
Now call EnginesRoot.AddStateMachine
to add State Machine, along with your Schema.
IndexedDB indexedDB = _enginesRoot.GeneratedIndexedDB();
GameSchema schema = _enginesRoot.AddSchema<GameSchema>(indexedDB);
CharacterFSM characterFSM = _enginesRoot.AddStateMachine<CharacterFSM>(indexedDB);
You can build entities as same and can set Initial State with it.
var builder = _schema.Character.Build(_factory, entityID);
builder.Init(new CharacterStateComponent(CharacterState.Normal));
But to make Transitions happen, make sure you call StateMachine.Step()
. StateMachine
is IStepEngine
so you have option to pass it to SortedEnginesGroup
, etc.
Lastly, you can query Entities by calling Where()
with State Machine object. Same as you do with Indexes!
characterFSM.Step();
foreach (var result in indexedDB.Select<RageSet>()
.From(schema.Character)
.Where(characterFSM.Is(CharacterState.Angry)))
{
// ...
}
We learned how to define and query with State Machine, and how to configure States, Transitions, Conditions, Callbacks. In Next Document, we will look how to use Foreign Key
and Join
query through it.