Project | Package |
---|---|
StateMachine | |
MonoGameStateMachine (deprecated) Please use the normal StateMachine instead |
Use the normal StateMachine and pass a TimeSpan on Update. |
Java-Version: JavaStateMachine | Olards Java Version on GitHub |
We simply didn't want to maintain two libraries of this size and the non-MG users wanted the functionality of the MonoGame version as well. So we compromised and ported the MG version back to the core-version replacing the GameTime
reference, which really was the only thing we used from MG, which felt like a waste, with a TimeSpan
everyone may use. Use it like this:
machine.Update(TimeSpan.FromMilliseconds(gameTime.ElapsedGameTime.TotalMilliseconds));
This project provides a Finite-State-Machine (FSM) as a portable class library (PCL) designed to be used in games.
Furthermore it implements even a Stack-Based-FSM (SBFSM). So you may tell it to 'continue with the last state before the active one'.
You describe your FSM using a nice and well documented DSL (Domain Specific Language).
If you're looking for a Java version of this project, check out Olards Java Version on GitHub.
If you like this repo, please don't forget to star it. Thank you.
Is the generic implementation in the form of a PCL (portable code library). It references no other library (no dependencies).
Nice if you want to use it outside of MonoGame.
This replaces the code we usually had for keyboard-input (run-left-right-duck-jump), clicked buttons on the GUI (idle-over-down-refreshing), tower-states (idle-aiming-firing-reloading) or for the connection procedure when setting up peer2peer connections in our games (more complex; example further down).
The idea is to generate a single FSM for every 'layer' of input that your engine allows. In our example it's a multi-button GUI. Some of the buttons will stay pressed and the GUI will enter a 'selection-grid-mode' until the left button is pressed again. Some of them just do immediate actions and become immediately released afterwards. Some of both of those have a refresh-time and stay disabled for that period. In this example the state of the GUI ('selection-grid-mode' or not) would be such a machine.
We place those machines in a single class where they could 'talk with each other' by reading their respective states. That way it is possible to construct and react on compound states.
Fsm<State, Trigger>.Builder(State.STANDING)
.State(State.DUCKING)
.TransitionTo(State.STANDING).On(Trigger.UP)
.State(State.STANDING)
.TransitionTo(State.DUCKING).On(Trigger.DOWN)
.TransitionTo(State.JUMPING).On(Trigger.UP)
.State(State.JUMPING)
.TransitionTo(State.DIVING).On(Trigger.DOWN)
.State(State.DIVING)
.Build();
A nice and more complex example for such a machine is the setup of a multiplayer game. It would be like the following:
- Send the 'load level' signal to other players
- Load level
- Display 'waiting for other players' message-box
- Wait for all other players to finish loading
- Send the level-data to other players and wait for acknowledgement from each of them
- Remove the 'waiting for other players' message-box
- Send 'start' signal to other players
- Start the game
Time to take it for a test-drive.
The motivation for this project came from a nice article I found here which comes with some examples. We tried to solve the proposed problems with our new project.
By the way: This seems to be a great book, so try to support the author in any way possible for you.
He's making a point using a FSM that looks like this:
- ducking --(release down)--> standing
- standing --(press down)--> ducking
- standing --(press B)--> jumping
- jumping --(press down)--> diving
So the file GameProgrammingPatterns1.cs
in the test-folder contains that machine.
This is a short paragraph that is about an example what configuring a state machine actually looks like.
private enum VState { DUCKING, STANDING, JUMPING, DESCENDING, DIVING };
private enum VTrigger { DOWN_RELEASED, DOWN_PRESSED, UP_PRESSED, SPACE_PRESSED };
private enum HState { STANDING, RUNNING_LEFT, RUNNING_RIGHT, WALKING_LEFT,
WALKING_RIGHT, WALKING_DELAY_LEFT, WALKING_DELAY_RIGHT};
private enum HTrigger { LEFT_PRESSED, LEFT_RELEASED, RIGHT_PRESSED, RIGHT_RELEASED, SPACE_PRESSED };
private Fsm<HState, VTrigger> verticalMachine;
private Fsm<VState, HTrigger> horizontalMachine;
private Keys[] lastKeysPressed;
private Hero hero;
public void main() {
horizontalMachine = Fsm<HState, HTrigger>.Builder(STANDING)
.State(STANDING)
.TransitionTo(WALKING_LEFT).On(LEFT_PRESSED)
.TransitionTo(WALKING_RIGHT).On(RIGHT_PRESSED)
.OnEnter(e => {
ConsoleOut();
hero.HAnimation = HAnimation.STANDING;
hero.delayTimer.StopAndReset();
})
.State(WALKING_LEFT)
.TransitionTo(WALKING_DELAY_LEFT).On(LEFT_RELEASED)
.OnEnter(e => {
ConsoleOut();
hero.HAnimation = HAnimation.WALK_LEFT;
hero.delayTimer.StopAndReset();
})
.State(WALKING_RIGHT)
.TransitionTo(WALKING_DELAY_RIGHT).On(RIGHT_RELEASED)
.OnEnter(e => {
ConsoleOut();
hero.HAnimation = HAnimation.WALK_RIGHT;
hero.delayTimer.StopAndReset();
})
.State(WALKING_DELAY_LEFT)
.TransitionTo(WALKING_RIGHT).On(RIGHT_PRESSED)
.TransitionTo(RUNNING_LEFT).On(LEFT_PRESSED)
.OnEnter(e => {
hero.delayTimer.Start();
})
.Update(a => {
hero.delayTimer.Update(a.ElapsedTimeSpan);
if(hero.delayTimer) {
horizontalMachine.JumpTo(STANDING);
}
})
.State(WALKING_DELAY_RIGHT)
.TransitionTo(WALKING_LEFT).On(LEFT_PRESSED)
.TransitionTo(RUNNING_RIGHT).On(RIGHT_PRESSED)
.OnEnter(e => {
hero.delayTimer.Start();
})
.Update(a => {
hero.delayTimer.Update(a.ElapsedTimeSpan);
if(hero.delayTimer) {
horizontalMachine.JumpTo(STANDING);
}
})
.State(RUNNING_LEFT)
.TransitionTo(STANDING).On(LEFT_RELEASED)
.OnEnter(e => {
ConsoleOut();
hero.HAnimation = HAnimation.RUNNING_LEFT;
hero.delayTimer.StopAndReset();
})
.State(RUNNING_RIGHT)
.TransitionTo(STANDING).On(RIGHT_RELEASED)
.OnEnter(e => {
ConsoleOut();
hero.HAnimation = HAnimation.RUNNING_RIGHT;
hero.delayTimer.StopAndReset();
})
.GlobalTransitionTo(STANDING).On(SPACE_PRESSED)
.Build();
verticalMachine = Fsm<VState, VTrigger>.Builder(STANDING)
.State(STANDING)
.TransitionTo(DUCKING).On(DOWN_PRESSED)
.TransitionTo(JUMPING).On(UP_PRESSED)
.OnEnter(e => {
ConsoleOut();
hero.VAnimation = VAnimation.IDLE;
})
.OnExit(Console.Out.WriteLine($"From [{e.From}] with [{e.Input}] to [{e.To}]"))
.State(DUCKING)
.TransitionTo(STANDING).On(DOWN_RELEASED)
.OnEnter(e => {
ConsoleOut();
hero.VAnimation = VAnimation.DUCKING;
})
.OnExit(ConsoleOut)
.State(JUMPING)
.TransitionTo(DIVING).On(DOWN_PRESSED)
.OnEnter(e => {
ConsoleOut();
hero.VAnimation = VAnimation.JUMPING;
})
.OnExit(ConsoleOut)
.Update(a => {
hero.height += a.ElapsedTimeSpan.TotalSeconds * 100F;
if(hero.height >= 200F)
verticalMachine.TransitionTo(DESCENDING);
})
.State(DESCENDING)
.TransitionTo(DIVING).On(DOWN_PRESSED)
.OnEnter(e => {
ConsoleOut();
hero.VAnimation = VAnimation.DESCENDING;
})
.OnExit(ConsoleOut)
.Update(a => {
hero.height -= a.ElapsedTimeSpan.TotalSeconds * 100F;
if(hero.height <= 0F) {
hero.height = 0F;
verticalMachine.TransitionTo(STANDING);
}
})
.State(DIVING)
.TransitionTo(DESCENDING).On(DOWN_RELEASED)
.OnEnter(e => {
ConsoleOut();
hero.VAnimation = VAnimation.DIVING;
})
.OnExit(ConsoleOut)
.Update(a => {
hero.height -= a.ElapsedTimeSpan.TotalSeconds * 150F;
if(hero.height <= 0F) {
hero.height = 0F;
verticalMachine.TransitionTo(STANDING);
}
})
.GlobalTransitionTo(STANDING).On(SPACE_PRESSED)
.Build();
}
protected override void Update(GameTime gameTime) {
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
Keyboard.GetState().IsKeyDown(Keys.Escape))
Exit();
var s = Keyboard.GetState();
if (s.IsKeyDown(Keys.Up))
verticalMachine.Trigger(UP_PRESSED);
if (s.IsKeyDown(Keys.Down))
verticalMachine.Trigger(DOWN_PRESSED);
if (!s.IsKeyDown(Keys.Down) && lastKeysPressed.Contains(Keys.Down))
verticalMachine.Trigger(DOWN_RELEASED);
if (s.IsKeyDown(Keys.Left))
horizontalMachine.Trigger(LEFT_PRESSED);
if (s.IsKeyDown(Keys.Right))
horizontalMachine.Trigger(RIGHT_PRESSED);
if (!s.IsKeyDown(Keys.Right) && lastKeysPressed.Contains(Keys.Right))
horizontalMachine.Trigger(RIGHT_RELEASED);
if (!s.IsKeyDown(Keys.Left) && lastKeysPressed.Contains(Keys.Left))
horizontalMachine.Trigger(LEFT_RELEASED);
lastKeysPressed = s.GetPressedKeys();
// Update the machines themselves.
verticalMachine.Update(TimeSpan.FromMilliseconds(
gameTime.ElapsedGameTime.TotalMilliseconds));
horizontalMachine.Update(TimeSpan.FromMilliseconds(
gameTime.ElapsedGameTime.TotalMilliseconds));
}
private void ConsoleOut(TransitioningValueArgs<string> e) {
Console.Out.WriteLine($"From [{e.From}] with [{e.Input}] to [{e.To}]");
}
Another example with a spell-button that has a refresh-time:
private enum State { IDLE, OVER, PRESSED, REFRESHING };
private enum Trigger { MOUSE_CLICKED, MOUSE_RELEASED, MOUSE_OVER, MOUSE_LEAVE };
private Dictionary<Button, Fsm<State, Trigger>> buttonMachines = new
Dictionary<Button, Fsm<State, Trigger>>();
private void CreateMachineFor(Button button)
buttonMachines.Add(button, Fsm.Builder<State, Trigger>(IDLE)
.State(IDLE)
.TransitionTo(OVER).On(MOUSE_OVER)
.OnEnter(e => {
button.State = ButtonState.IDLE;
})
.State(OVER)
.TransitionTo(IDLE).On(MOUSE_LEAVE)
.TransitionTo(PRESSED).On(MOUSE_CLICKED)
.OnEnter(e => {
button.State = ButtonState.OVER;
})
.State(PRESSED)
.TransitionTo(IDLE).On(MOUSE_LEAVE).If(button.Kind == Kind.FLIPBACK)
.TransitionTo(REFRESHING).On(MOUSE_RELEASED)
.OnEnter(e => {
button.State = ButtonState.DOWN;
})
.State(REFRESHING)
.OnEnter(e => {
hero.doSpell(button.DoAssociatedSpell());
button.RefreshTimer.Start();
button.State = ButtonState.REFRESHING;
})
.Update(a => {
if(button.RefreshTimer.Value <= 0F) {
button.RefreshTimer.StopAndReset();
machine.JumpTo(IDLE);
}
})
.Build();
}
public void main() {
Button b1 = new Button("name1", "someText", ...);
Button b2 = new Button("name2", "someOtherText", ...);
CreateMachineFor(b1);
CreateMachineFor(b2);
...
}
There also is the After
feature ported from the MG version. Use it like that:
Fsm<State, Trigger>.Builder(State.STANDING)
.State(State.DUCKING)
.TransitionTo(State.STANDING).After(TimeSpan.fromMilliseconds(500))
...
This functionality is achieved by updating the After
conditions before evaluating the Update
function - Be advised that this happens directly before the Update
call with the TimeSpan
you've specified in the call to Update
. If the After
function triggers, the call to Update
will be omitted.
- Fluent-State-Machine - by RoryDungan (MIT License)
- Nate - by mmonteleone (MIT License)
- PlantUML - PlantText - PlantUML website. You can layout your PlantUML text and generate images out of them there.