A light-weight state machine language, parser and interpreter built for run-time compilation and execution. It's designed to create simple state machine's out of reusable components instead of custom coded states.
Though Transition was designed for Unity development it has no dependencies other than System
(and a few child namespaces) and will work for any C# based project.
This library is fully compatible with iOS/Android Unity development.
Describe and execute state machines like this:
@machine SelfClosingDoor ->Closed @state Closed @enter SetAnimation 'closed' @on 'OpenDoor': ->Open @state Open @enter SetAnimation 'open' @run WaitForAnimation Wait seconds:'3' ->Closed
An example project in Unity.
A guide for all the basic components needed to get up and running in Unity.
In atom, install the package 'transition-grammar'. To make it work save your files with a .tx extension or choose the 'transition' language when manually selecting your grammar.
transition-grammar atom package
- Start Here
- [Get the Latest Release](#get-the-latest-releasehttpsgithubcombcatchotransitionreleases)
- [Unity Transition Examples](#unity-transition-exampleshttpsgithubcombcatchotransition-unity-examples)
- [Tutorial: Setting up in Unity](#tutorial-setting-up-in-unityhttpsgithubcombcatchotransitionwikitutorial-setting-up-in-unity)
- Watch an introduction video
- Get the atom syntax highlighting package
- Language concepts
- Ticking
- Messaging
- MachineController
- Value Converters
- Contexts
- Default Parameters
- AltId
- Blackboard
- An example in excruciating detail
- Contributing
A Machine (state machine) in Transition looks like this:
@machine MachineName ->FirstState
@on
'sing': say 'lalalala'
@state FirstState
@enter
DoSomethingOnEnter
@run
say 'hi'
moveRight speed:'3' distance:'2'
->StateTwo
@exit
say message:'bye'
@on
'poke': say 'ouch!'
@machine Name -> StateName
The first line in a machine describes the name of the machine and the first state to transition toward. This line is required in all machines.
A Machine can have global message handlers. These are placed in an optional @on
section beneath the
@machine
line. See the @on section for more details.
@state Name
A state has a name and 4 sections: @enter
, @run
, @exit
, @on
. A state can
exist without anything but this first line.
@state Name
@run
Action
Action2
The @run
section describes a list of Actions to run in order. Every time the
machine is ticked the current state will run the active Action. If all Actions have
run the state will not perform any Actions on subsequent ticks. Actions are the primary
way of controlling behavior and are discussed below.
@state Name
@enter
Action
Action2
@exit
Action2
Action3
The @enter
and @exit
state sections describe a list of Actions to run in order when
a state is entered or exited and only then.
Actions in this section are special. They must return TickResult.Done()
. In other words
they must end in the same tick they are run and can not transition to a different state. This
is discussed in the Ticking section.
@state Name
@on
'message': Action
'message2': Action
The @on
section is a special state section that describes a collection of messages
and associated Actions. Each message can have only one Action. When a Machine is sent an
Action it will look in this section to see if the current state can handle it. If so the
associated Action will be run.
Actions in this section are special. They must return TickResult.Done()
or transition.
This is discussed in the Messaging section.
@state Name
@run
Action param:'3' transitionParam->OtherState
Actions are the part that makes everything work. They are associated with a c# class that you must write. Actions can accept as many parameters as you wish including special Transition Parameters.
When implementing an Action there are 4 return types to choose from. Each affects the Machine differently and all control the flow of behavior in some way.
-
Done specifies that the Action is done and that the next (if any) should be run. Use
TickResult.Done()
to create this type of result. -
Yield specifies that the Action is not done processing and should be run again on the next Tick. This allows actions to execute over many Ticks (or in-game time). Useful for behavior like movement, delays, animation. Use
TickResult.Yield()
to create this type of result. -
Transition tells the machine to transition to a state. To specify which state to transition to you must use a
TransitionDestination
. This is described in the section on Transition Parameters. Use the protected methodTransitionTo()
to return aTransition
result. -
Loop tells the current state to run the first action in the section on the very next Tick.
Example actions:
public class SimpleAction : Action<Context> {
protected override TickResult OnTick(Context context) {
// return done and move on to the next action
return TickResult.Done();
}
}
public class LongRunningAction : Action<Context> {
protected override TickResult OnTick(Context context) {
// always returning yield means the next action will never be run
return TickResult.Yield();
}
}
public class TransitionAction : Action<Context> {
public TransitionDestination NextState {get; set;}
protected override TickResult OnTick(Context context) {
// TransitionTo is a built in method to help build a Transition TickResult
return TransitionTo(NextState);
}
}
public class LoopingAction : Action<Context> {
protected override TickResult OnTick(Context context) {
// this will restart the state at the very first @run action in the next tick
return TickResult.Loop();
}
}
An action can have many parameters. Each parameter is associated with a property in the
backing class. For instance an action that takes an c# int
and string
called a
and b
respectively
would look like this:
exampleAction a:'200' b:'woof'
public class ExampleAction : Action<Context> {
public int A {get; set;}
public string B {get; set;}
//... the rest of the action
}
The few types that are supported by default are: float
, int
, string
. However, you can
support any type of object or value just by implementing a IValueConverter
. See the section
on Value Converters for details.
If you want your state to transition to another (or the same) you'll need to use a special
type of parameter. Any state name that occurs after an arrow ->
will be assigned to that
parameter. The parameter must be backed by a property with the TransitionDestination
type.
For example:
exampleAction a->NextState
public class ExampleAction : Action<Context> {
public TransitionDestination A {get; set;}
//... the rest of the action
}
Comments are made by using the #
symbol. These are all valid comments:
#comment on it's own line
@state Name #comment at end of line
@run
#comments inside of sections
"Ticking" is the process of running a Machine. When a Machine is ticked the current state's current action is executed and based on the TickResult returned an action is taken.
Certain actions happen can happen in the same tick, but for performance and conceptual reasons some can't.
When a state transitions to another, the @exit
and @enter
sections are executed.
The @run
section of the new state is not executed until the next tick:
Tick 1
- An action in State A causes transition to State B
- All Actions in State A's
@exit
section are executed in order - All Actions in State B's
@enter
section are executed in order
Tick 2
The first Action in State B's @run
section is executed (and processing occurs normally).
This behavior is meant to reduce the need for an implementor to worry about the performance of the Machine. Without this boundary it would be very easy to create infinite loops between states.
When a Machine is first ticked it must transition to the initial state. This
follows all the rules of the Transition Tick Boundary above. Thus, the first
Action in the Initial State's @run
section won't be executed until the second tick.
Yielding (see section on return types) tells the machine to stop processing and start at the same Action on the next tick. It is the simplest form of a tick boundary.
Returning Done
from an action does not cause a tick boundary. The next Action in the same section will be run immediately.
Returning Loop
does cause a tick boundary. The first Action in the same section will be run on the next tick. This is to help prevent infinite loops.
TODO
TODO
TODO
TODO
TODO
TODO
TODO
Let's demonstrate some important language features with a an example of a Machine for a door that closes after three seconds. The examples pretend you are working in a Unity-like environment. We will start from the product:
Here is an example machine
@machine SelfClosingDoor ->Closed @state Closed @enter SetAnimation 'closed' @on 'OpenDoor': ->Open @state Open @enter SetAnimation 'open' @run WaitForAnimation Wait seconds:'3' -> Closed
Here is all the code it takes to power it
public class SetAnimation : Action<UnityContext>
{
[DefaultParameter]
public string AnimationVal { get; set; }
protected override TickResult OnTick (UnityContext context) {
context.GameObject.GetComponent<Animator>().Play(AnimationVal);
return TickResult.Done();
}
}
public class WaitForAnimation : Action<UnityContext>
{
protected override TickResult OnTick (UnityContext context) {
var animator = context.GameObject.GetComponent<Animator>();
return animator.IsAnimating() ? TickResult.Yield() : TickResult.Done();
}
}
public class Wait : Action<UnityContext>
{
public float Seconds { get; set; }
protected override void OnEnterAction (UnityContext context) {
context.BlackBoard.Set<float>("elapsedTime", 0);
}
protected override TickResult OnTick (UnityContext context) {
var elapsedTime = context.BlackBoard.Get<float>("elapsedTime");
elapsedTime += Time.deltaTime;
context.BlackBoard.Set<float>("elapsedTime", elapsedTime);
return elapsedTime > Seconds ? TickResult.Done() : TickResult.Yield();
}
}
Now we can dissect it section by section, line by line.
@machine SelfClosingDoor ->Closed
This line is very straight forward and has three components: @machine
keyword, a Name for the machine and which state to transition the first time it is run.
1 @state Closed 2 @enter 3 SetAnimation 'closed' 4 @on 5 'OpenDoor': ->Open
This State is more interesting. Let's break it down line by line.
line | description |
---|---|
1 | The first line declares a new state and it's name. |
2 | On the second line we start a new section, in particular the @enter section. Any Actions in this section will be run when the state is first entered. |
3 | The next line contains an Action. This is the bit that you would write code for. In this case it is an Action that sets some animation state. |
4 | Now we have reached a new section that is slightly special. An @on section contains a list of Actions that will be run in response to an incoming message. In this case the Action is simply to transition to the 'Open' state. |
5 | Finally we have a message-Action tuple that instructs the State to transition to the Open State when the OpenDoor message is received if the door was unlocked. Any Action can be used in response to a message, not just Actions that transition. In fact this Action doesn't have to transition (which we will see later). |
1 @state Open 2 @enter 3 SetAnimation 'open' 4 @run 5 WaitForAnimation 6 Wait seconds:'3' 7 -> Closed
This state demonstrates even more language features. We will skip over those already mentioned above.
line | description |
---|---|
3 | This line is only interesting because it demonstrates Action reuse. Even though the Action contains a different parameter value, the code to make it work only needs to be written once. |
4 | The @run section is executed every frame in order. Actions in the Run section can take as many frames as is necessary to complete. |
5 | The WaitForAnimation Action will only exit once the door opening animation is complete. Thus, it may take a few frames before we reach line 6 |
6 | The Wait Action takes a parameter that indicates how many seconds to wait before moving to the next Action. |
7 | The -> closed is actually syntactic sugar for $trans -> closed . It's a special built in Action that makes transitioning to another state really easy to type. |
What follows is all the code necessary to power the Machine. While there is a little more code necessary to make it work in your game or simulation, it is the least interesting part.
The SetAnimation
Action is a great example of Action reuse. It is a small Action that plays an animation and exits. The OnTick
method is what is called by the Machine Controller when it run's your code. It uses the TickResult
to determine if it should run the next Action in the state or not. We will see examples of that later.
Note the DefaultParameter
attribute - this allows for syntactic sugar: SetAnimation AnimationVal:'blah'
is equivalent to SetAnimation 'blah'
.
The WaitForAnimation
Action checks the status of an animation and returns either Yield
or Done
. This is one of the interesting properties of Actions in the @run
section - they can last many ticks.
This was included in the example to demonstrate that Actions are run in sequence, but only after the previous has run. It also uses the BlackBoard (albeit in a very inefficient way) to store state. Actions are essentially stateless and use the Context to store state. This allows Machine's to be instantiated once and reused many times.
As you can see, the Action concept promotes re-use of common tasks that happen in State Machine's. There are still some interesting concepts to explain such as the Machine Controller, MessageBus or transitioning but this will do for an initial demonstration.
It's very early but feel free to open issues and leave comments on any bugs or feature requests. See design.md for details, guidelines and philosophies.