📜 ⬆️ ⬇️

Pattern state machines for Ash Entity System framework

Two years ago, I got acquainted with the most wonderful framework for developing games on Actionscript Ash (almost immediately after meeting him, it came to be understood that the framework would suit any language. And even a little later, the ports of the framework for other languages ​​appeared - at the time of writing the translation there are 7 pieces). For some time, I was bursting with the desire to share the find with the Russian-speaking community, until I was ahead of the transfer to Habré . Now I want to put in a translation regarding the implementation of the almost indispensable Finite-State Machine pattern for the Ash Entity System framework.

Note: the naming of basic concepts from the Entity System framework (ES framework) will be written with a capital letter - to avoid possible ambiguities: System ( System ), Component ( Component ), Node ( Entity )

Pattern state machines for Ash Entity System framework


Finite state machines (FSM) - one of the main constructions in game dev. During the game, game objects can repeatedly change their states and effective management of these states is very important.

The complexity of the FSM in the ES framework can be expressed in one sentence - FSM does not work with the ES framework. Any ES framework uses a data-oriented approach (data-oriented paradigm), in which gaming entities are not encapsulated OOP objects. Thus, you will not be able to use FSM or its variants. All data is in Components, all logic is in Systems.
')
If there are few states or they are simple, then you can use the good old switch statement inside the System, if the data for all possible states of the gaming entity (enclosed in the corresponding Components) are also used by this System. But I would not recommend it.

When developing Stick Tennis, I faced the problem of managing states. The sequence of changes in these states was similar to something similar ...


Stick Tennis is a complex example, and I cannot show the program code, instead I will show something simpler.

Example

Let's imagine the game character - the guard. A guard patrols along a route, looking around carefully. When an enemy is detected, it attacks.

In traditional OOP-oriented FSM, we need to create classes for each state.

public class PatrolState { private var guard : Character; private var path : Vector.<Point>; public function PatrolState( guard : Character, path : Vector.<Point> ) { this.guard = guard; this.path = path; } public function update( time : Number ) : void { moveAlongPath( time ); var enemy : Character = lookForEnemies(); if( enemy ) { guard.changeState( new AttackState( guard, enemy ) ); } } } 

 public class AttackState { private var guard : Character; private var enemy : Character; public function AttackState( guard : Character, enemy : Character ) { this.guard = guard; this.enemy = enemy; } public function update( time : Number ) : void { guard.attack( enemy ); if( enemy.isDead ) { guard.changeState( new PatrolState( guard, PatrolPathFactory.getPath( guard.id ) ); } } } 


In the Entity System architecture, we must take a slightly different approach. In this case, the basic principle of FSM, where it is possible to change states by means of a set of corresponding classes, when a particular state corresponds to each class, can still be applied. To implement FSM in the ES framework in this case we can use one System for one particular state.

 public class PatrolSystem extends ListIteratingSystem { public function PatrolSystem() { super( PatrolNode, updateNode ); } private function updateNode( node : PatrolNode, time : Number ) : void { moveAlongPath( node ); var enemy : Enemy = lookForEnemies( node ); if( enemy ) { node.entity.remove( Patrol ); var attack : Attack = new Attack(); attack.enemy = enemy; node.entity.add( attack ); } } } 

 public class AttackSystem extends ListIteratingSystem { public function AttackSystem() { super( AttackNode, updateNode ); } private function updateNode( node : PatrolNode, time : Number ) : void { attack( node.entity, node.attack.enemy ); if( node.attack.enemy.get( Health ).energy == 0 ) { node.entity.remove( Attack ); var patrol : Patrol = new Patrol(); patrol.path = PatrolPathFactory.getPath( node.entity.name ); node.entity.add( patrol ); } } } 

PatrolSystem will be responsible for the behavior of the guard character , if the PatrolComponent Component is included in it, and AttakSystem will be responsible for the behavior during the Attack Component component, if it is included in it. By changing these components, we can switch the status of the character guard.

These Components and Nodes will look something like this ...

 public class Patrol { public var path : Vector.<Point>; } public class Attack { public var enemy : Entity; } public class Position { public var point : Point; } public class Health { public var energy : Number; } public class PatrolNode extends Node { public var patrol : Patrol; public var position : Position; } public class AttackNode extends Node { public var attack : Attack; } 

So, by changing the Components of the Entity, we change the states of the Entity and System that are responsible for their behavior.

Another example

Here I will present another example, more complex from the game Asteroids example game , with the help of which I illustrate the work of the Ash Framework . I had to add another state for the spacecraft - the death of the ship. Instead of simply removing the spacecraft at the time of death, I show a short animation of its destruction. While I am demonstrating this animation, the player is deprived of the ability to control the ship, and the ship itself is not involved in handling collisions with other objects.

This required the following two states of the ship:

  1. the ship is alive
    • looks like a spaceship
    • the player can control it
    • the player can fire his weapon
    • the ship may collide with asteroids
  2. the ship is dead
    • looks like wreckage drifting in space
    • the player cannot control it
    • the player cannot fire his weapon
    • the ship cannot collide with asteroids
    • after a certain time the ship is removed from the game


The corresponding code where the ship is killed is in the CollisionSystem . Without the second state, it looks like this:
 for ( spaceship = spaceships.head; spaceship; spaceship = spaceship.next ) { for ( asteroid = asteroids.head; asteroid; asteroid = asteroid.next ) { if ( Point.distance( asteroid.position.position, spaceship.position.position ) <= asteroid.position.collisionRadius + spaceship.position.collisionRadius ) { creator.destroyEntity( spaceship.entity ); break; } } } 

The code checks whether the ship collided with an asteroid, and if this happened, removes the ship from the game. On the other hand, GameManager handles the situation when there is no space ship in the game and creates a new one. If all “lives” are exhausted - the end of the game. So - instead of simply removing the ship from the game, we have to change its state — show flying debris. Let's try…

We can deprive the player of control by simply removing the components of MotionControls and GunControls from the Essence of the spacecraft. We also need to remove the Motion and Gun components, since they are still not used without the appropriate controllers. Those. we measure the code above:
 for ( spaceship = spaceships.head; spaceship; spaceship = spaceship.next ) { for ( asteroid = asteroids.head; asteroid; asteroid = asteroid.next ) { if ( Point.distance( asteroid.position.position, spaceship.position.position ) <= asteroid.position.collisionRadius + spaceship.position.collisionRadius ) { spaceship.entity.remove( MotionControls ); spaceship.entity.remove( Motion ); spaceship.entity.remove( GunControls ); spaceship.entity.remove( Gun ); break; } } } 

Then we need to change the appearance of the ship (on the wreckage) and cancel the handling of collisions:
 for ( spaceship = spaceships.head; spaceship; spaceship = spaceship.next ) { for ( asteroid = asteroids.head; asteroid; asteroid = asteroid.next ) { if ( Point.distance( asteroid.position.position, spaceship.position.position ) <= asteroid.position.collisionRadius + spaceship.position.collisionRadius ) { spaceship.entity.remove( MotionControls ); spaceship.entity.remove( Motion ); spaceship.entity.remove( GunControls ); spaceship.entity.remove( Gun ); spaceship.entity.remove( Collision ); spaceship.entity.remove( Display ); spaceship.entity.add( new Display( new SpaceshipDeathView() ) ); break; } } } 

Finally, we need to ensure that the wreckage of the ship is removed after some period of time. For this we need a new system and component:
 public class DeathThroes { public var countdown : Number; public function DeathThroes( duration : Number ) { countdown = duration; } } public class DeathThroesNode extends Node { public var death : DeathThroes; } public class DeathThroesSystem extends ListIteratingSystem { private var creator : EntityCreator; public function DeathThroesSystem( creator : EntityCreator ) { super( DeathThroesNode, updateNode ); this.creator = creator; } private function updateNode( node : DeathThroesNode, time : Number ) : void { node.death.countdown -= time; if ( node.death.countdown <= 0 ) { creator.destroyEntity( node.entity ); } } } 

We add DeathThroesSystem to the game from the very beginning, and at the right moment it will react to the “death” of the Essence. It remains to add the DeathThroes Component to the Essence of the spacecraft.
 for ( spaceship = spaceships.head; spaceship; spaceship = spaceship.next ) { for ( asteroid = asteroids.head; asteroid; asteroid = asteroid.next ) { if ( Point.distance( asteroid.position.position, spaceship.position.position ) <= asteroid.position.collisionRadius + spaceship.position.collisionRadius ) { spaceship.entity.remove( MotionControls ); spaceship.entity.remove( Motion ); spaceship.entity.remove( GunControls ); spaceship.entity.remove( Gun ); spaceship.entity.remove( Collision ); spaceship.entity.remove( Display ); spaceship.entity.add( new Display( new SpaceshiopDeathView() ) ); spaceship.entity.add( new DeathThroes( 5 ) ); break; } } } 

As a result, we get the necessary state of the spacecraft. The transition between the states itself was provided by “shuffling” the components of the spacecraft.

The specific state is encapsulated in the Component Set.


The main rule of the Entity System Architecture is the Entity state encapsulated in the Component Set. If you want to change the behavior of the Entity, you must change the set of Components. Changing the set of Components in turn will change the set of Systems - and the behavior of the Entity will change.

Standardized code for FSM

For ease of working with FSM, I added a corresponding set of classes to the framework - standard state machine classes . A set of these classes will help identify new states, manage them.

FSM is an instance of the EntityStateMachine class. When you create an instance, you give it a link to the Entity, the states of which it will manage. The EntityStateMachine instance itself is usually stored in some Component in the Entity, and thus any System has access to it.
 var stateMachine : EntityStateMachine = new EntityStateMachine( guard ); 

FSM stores states, and these states can be changed by calling the EntityStateMachine.changeState () method. Each specific state is identified by a string (name) that is associated with the state when it was created and used when calling EntityStateMachine.changeState (stateName) .
 var patrolState : EntityState = stateMachine.createState( "patrol" ); var attackState : EntityState = stateMachine.createState( "attack" ); 


What is the state added to the FSM for?
  1. adds the necessary Components when the Entity enters this state;
  2. removes previously added components when leaving the current state.


The EntityStateMachine.add () method sets the Component type necessary for the defined state and follows the rules indicating how to create this Component.
 var patrol : Patrol = new Patrol(); patrol.path = PatrolPathFactory.getPath( node.entity.name ); patrolState.add( Patrol ).withInstance( patrol ); attackState.add( Attack ); 

There are four standard methods:


entityState.add (type: Class);
Without any indication, FSM will create a new instance of the Component of this type, for later adding it to the Entity. And it will be repeated each time with repeated returns to this state.
entityState.add (type: Class) .withType (otherType: Class);
This instruction creates a new instance of otherType each time it enters this state. otherType must be of type type or an extension of type
entityState.add (type: Class) .withInstance (instance: *);
This method makes it possible to use the same instance of the Component class when returning to this state.

And finally

entityState.add (type: Class) .withSingleton ();
or
entityState.add (type: Class) .withSingleton (otherType: Class);
will create a singleton and will use it every time it returns to this state. This is the same as using the withInstance method, but the withSingleton method will not create an instance of the class until it is needed. If otherType is not specified, a singleton of type is created. If otherType is specified, it must be of type type , or extend it.

Finally, you can define the Component yourself by implementing the IComponentProvider interface .
entityState.add (type: Class) .withProvider (provider: IComponentProvider);
The IComponentProvider interface is defined as follows.
 public interface IComponentProvider { function getComponent() : *; function get identifier() : *; } 

The getComponent method returns an instance of the component. The identifier property is used to compare two providers to ensure that they return the same Component. This is necessary in order to exclude the replacement of the Component, if two different states use the same Component.

The methods are designed to be “chained” so that you can create flexible interfaces, as you will see in the following example.

Back to examples

If we apply these new tools for example with a space ship, the code will take the following form

 var fsm : EntityStateMachine = new EntityStateMachine( spaceshipEntity ); fsm.createState( "playing" ) .add( Motion ).withInstance( new Motion( 0, 0, 0, 15 ) ) .add( MotionControls ) .withInstance( new MotionControls( Keyboard.LEFT, Keyboard.RIGHT, Keyboard.UP, 100, 3 ) ) .add( Gun ).withInstance( new Gun( 8, 0, 0.3, 2 ) ) .add( GunControls ).withInstance( new GunControls( Keyboard.SPACE ) ) .add( Collision ).withInstance( new Collision( 9 ) ) .add( Display ).withInstance( new Display( new SpaceshipView() ) ); fsm.createState( "destroyed" ) .add( DeathThroes ).withInstance( new DeathThroes( 5 ) ) .add( Display ).withInstance( new Display( new SpaceshipDeathView() ) ); var spaceshipComponent : Spaceship = new Spaceship(); spaceshipComponent.fsm = fsm; spaceshipEntity.add( spaceshipComponent ); fsm.changeState( "playing" ); 


As a result, the change of the current State will be simplified as much as possible:

 for ( spaceship = spaceships.head; spaceship; spaceship = spaceship.next ) { for ( asteroid = asteroids.head; asteroid; asteroid = asteroid.next ) { if ( Point.distance( asteroid.position.position, spaceship.position.position ) <= asteroid.position.collisionRadius + spaceship.position.collisionRadius ) { spaceship.spaceship.fsm.changeState( "destroyed" ); break; } } } 

Source: https://habr.com/ru/post/219007/


All Articles