17.2. Machine à états
Dans un exercice précédent, nous avons amélioré nos ennemis dans le but d'illustrer le concept de comportements avec des états. Bien que les états peuvent sembler superflus pour un ennemi simple, l'utilisation d'états est essentielle pour représenter un personnage intelligent complexe comme le boss. Lorsqu'il s'agit du boss pour un jeu vidéo, il est typique de vouloir avoir plusieurs phases d'attaque. Voici les différentes phases du combat et états du boss.
Shoot
Dans cet état le boss tire une série de projectiles en spirale
Chase
Dans cet était le boss pourchasse le joueur de façons similaires au comportement de l'ennemi.
Bounce
Dans cet état le boss rebondit sur les bordures du niveau de plus en plus vite.
Minions
Dans cet état, le boss fait apparaitre un groupe d'ennemi pour attaquer le joueur
Waypoint
Dans cet état, le boss se dirige vers un des points de contrôle le plus proche.
En résumé, il est utile de représenter les états par un diagramme.
Lorsque nous avons amélioré nos ennemis, nous avons utilisé un simple switch statement afin de représenter leurs comportements. En effet, la méthode setState
permet de changer et initialiser l'état de l'ennemi. La méthode updateState
permet de mettre à jour l'ennemi selon l'état de manière assez simple. Cependant lorsqu'il s'agit d'un grand nombre d'états ce simple mécanisme peut être très encombrant.
public class SimpleBossDemo extends Character {
public class StateUtils
{
public static final int SHOOT = 0;
public static final int CHASE = 1;
public static final int BOUNCE = 2;
public static final int MINIONS = 3;
public static final int WAYPOINT = 4;
public static final int DECIDE = 5;
public static final int IDLE = 6;
}
public void setState(int state)
{
switch (state)
{
case StateUtils.SHOOT:
// Plusieurs lignes de code ...
break;
case StateUtils.CHASE:
// Plusieurs lignes de code ...
break;
case StateUtils.MINIONS:
// Plusieurs lignes de code ...
break;
case StateUtils.WAYPOINT:
// Plusieurs lignes de code ...
break;
case StateUtils.DECIDE:
// Plusieurs lignes de code ...
break;
case StateUtils.IDLE:
// Plusieurs lignes de code ...
break;
}
this.state = state;
}
public void updateState(float deltaTime)
{
// ...
switch (state)
{
case StateUtils.SHOOT:
// Plusieurs lignes de code ...
break;
case StateUtils.CHASE:
// Plusieurs lignes de code ...
break;
case StateUtils.MINIONS:
// Plusieurs lignes de code ...
break;
case StateUtils.WAYPOINT:
// Plusieurs lignes de code ...
break;
case StateUtils.DECIDE:
// Plusieurs lignes de code ...
break;
case StateUtils.IDLE:
// Plusieurs lignes de code ...
break;
}
}
}
Une meilleure manière de procéder est d'encapsuler chacun des états à l'intérieur de classes. En d'autres mots, il est possible d'utiliser le polymorphisme afin de mettre à jour le boss en délégant le travail à une classe de type State
plutôt qu'au boss en tant que tel.
public class State {
public void enter()
{
this.state = state;
}
public void update(Boss boss, float deltaTime)
{
// ...
this.state.update(this, deltaTime);
}
}
public class BetterBossDemo extends Character {
public void setState(State state)
{
this.state = state;
}
public void updateState(float deltaTime)
{
// ...
this.state.update(this, deltaTime);
}
}
En pratique nous voulons être capables d'effectuer les opérations suivantes:
exit state
enter state
set state
get state name
update state
add state
Il est donc naturel de vouloir encapsuler le comportement relatif aux états dans une classe appart et non gérer les états à l'intérieur de Boss
. Une machine à états (State Machine) est un objet théorique utilisé en informatique et est utilisée précisément pour résoudre le problème de gestion des états et transitions. Les classes StateMachine
et State
peuvent êtres utilisés afin de facilement intégrer des états à notre jeu.
Afin d'ajouter un nouveau comportement à notre personnage, il s'agit que de dériver la classe State
et d'ajouter une instance au StateMachine
situe dans le personnage.
Étapes à suivre
- ajoutez la classe
Boss
- chargez la texture
assets/objects/boss_spritesheet.png
et ajoutez les animations nécessaires.- ajoutez les méthodes
update
etrender
de manière similaire à l'ennemi
/* Boss.java */
package com.tutorialquest.entities.boss;
// import ..
public class Boss extends Character {
public class SpriteUtils {
public static final int IDLE_FRONT = Character.SpriteUtils.IDLE_FRONT;
public static final int WALK_FRONT = Character.SpriteUtils.WALK_FRONT;
public static final int IDLE_SIDE = Character.SpriteUtils.IDLE_SIDE;
public static final int WALK_SIDE = Character.SpriteUtils.WALK_SIDE;
public static final int IDLE_BACK = Character.SpriteUtils.IDLE_BACK;
public static final int WALK_BACK = Character.SpriteUtils.WALK_BACK;
public static final int SHOOT = 20;
}
public static final int WIDTH = 64;
public static final int HEIGHT = 64;
public static final float MAX_HEALTH = 10;
public static final float DAMAGE = 15;
public static final float KNOCKBACK = 200f;
public static final float SPEED = 40f;
public static final float PUSH_FORCE = 0.8f;
public Vector2 destination = new Vector2();
private StateMachine stateMachine = new StateMachine();
public void initSprite() {
sprite = new AnimatedSprite(
"objects/boss_spritesheet.png",
new Vector2(WIDTH, HEIGHT));
sprite.origin = new Vector2(WIDTH/2, 0);
sprite.addAnimation(
SpriteUtils.IDLE_FRONT,
AnimatedSprite.DEFAULT_FRAME_LENGTH,
Animation.PlayMode.LOOP,
sprite.frames[0][1]
);
sprite.addAnimation(
SpriteUtils.WALK_FRONT,
AnimatedSprite.DEFAULT_FRAME_LENGTH,
Animation.PlayMode.LOOP,
sprite.frames[0][0],
sprite.frames[0][1],
sprite.frames[0][2]
);
sprite.addAnimation(
SpriteUtils.IDLE_SIDE,
AnimatedSprite.DEFAULT_FRAME_LENGTH,
Animation.PlayMode.LOOP,
sprite.frames[2][1]
);
sprite.addAnimation(
SpriteUtils.WALK_SIDE,
AnimatedSprite.DEFAULT_FRAME_LENGTH,
Animation.PlayMode.LOOP,
sprite.frames[2][0],
sprite.frames[2][1],
sprite.frames[2][2]
);
sprite.addAnimation(
SpriteUtils.IDLE_BACK,
AnimatedSprite.DEFAULT_FRAME_LENGTH,
Animation.PlayMode.LOOP,
sprite.frames[1][1]
);
sprite.addAnimation(
SpriteUtils.WALK_BACK,
AnimatedSprite.DEFAULT_FRAME_LENGTH,
Animation.PlayMode.LOOP,
sprite.frames[1][0],
sprite.frames[1][1],
sprite.frames[1][2]
);
sprite.play(SpriteUtils.WALK_FRONT, true);
}
public void initCollider()
{
collider = new Collider(
new Vector2(WIDTH/2, 16),
Collider.FLAG_COLLIDABLE | Collider.FLAG_ENEMY);
collider.origin = new Vector2(WIDTH/4, 0);
}
public void initLootTable()
{
lootTable = new LootTable();
lootTable.add(
new LootTable.Loot()
{{
type = Collectible.Type.Money;
probability = 1f;
value = Money.GOLD_STACK_VALUE;
}},
new LootTable.Loot()
{{
type = Collectible.Type.Money;
probability = 1f;
value = Money.DIAMOND_VALUE;
}}
);
}
public Boss(
Vector2 position)
{
super(position, DAMAGE, KNOCKBACK, MAX_HEALTH, SPEED, PUSH_FORCE);
initSprite();
initCollider();
initLootTable();
}
// Mise à jour de Boss de manière similaire à ennemi
@Override
public void update(float deltaTime) {
super.update(deltaTime);
updateVelocity(deltaTime);
turn();
push(Collider.FLAG_NONE);
collisionAttack(Collider.FLAG_AVATAR);
move();
collider.update(position);
}
}
Étapes à suivre
- ajoutez une machine à état
stateMachine
de typeStateMachine
àBoss
responsable pour le contrôle des états
- Initialiser le contenu à l'intérieur de la méthode
initStateMachine
- ajoutez le fichier
BossState.java
dans lequel nous définirons l'état de base ainsi que les identifiants.- pour commencer, ajoutez les états
IdleState
etDecisionState
DecisionState
permet d'obtenir une transition aléatoire lorsque le temps défini pour un état est écoulé.
- Cet état omniprésent est défini dans la classe
StateMachine
IdleState
prescrit à l'usager de rester immobile pour la durée de l'état/- chaque état est construit de la manière suivante:
StateMachine stateMachine
- référencé à la machine afin de pouvoir changer d'état
int id
- l'identifiant de l'état courant au sein de la machine
float probability
- probabilité de transition lors d'une décision
Decision
float timeLimit
- Limite de temps à l'état,
-1
si aucune limiteint timeoutState
- État de transition lorsque le temps est écoulé
/* BossState.java */
package com.tutorialquest.entities.boss;
// import ..
public static class BossState extends StateMachine.State
{
public static final int STATE_IDLE = 0;
public static final int STATE_DECIDE = 1;
public static final int STATE_WAYPOINT = 2;
public static final int STATE_SHOOT_SPIRAL = 3;
public static final int STATE_MINIONS = 4;
public static final int STATE_BOUNCE = 5;
public static final int STATE_CHASE = 6;
protected Boss boss;
public State(
Boss boss,
StateMachine stateMachine,
int id,
float probability,
float timeLimit,
int timeoutState)
{
super(
stateMachine,
id,
probability,
timeLimit,
timeoutState);
this.boss = boss;
}
}
/* IdleState.java */
package com.tutorialquest.entities.boss;
// import ..
public static class IdleState extends BossState
{
@Override
public String getName() {
return "Idle";
}
public IdleState(
Boss boss,
StateMachine
stateMachine,
int id,
float probability,
float timeLimit,
int timeoutState)
{
super(
boss,
stateMachine,
id,
probability,
timeLimit,
timeoutState);
}
// Immobilise le personnage
@Override
public void enter() {
super.enter();
boss.locomotionVelocity.setZero();
boss.controlAxes.setZero();
}
}
/* Boss.java */
package com.tutorialquest.entities.boss;
// import ..
public class Boss extends Character {
public void initStateMachine(){
stateMachine.addState(
new DecisionState(stateMachine, BossState.STATE_DECIDE, -1, -1, -1),
true);
stateMachine.addState(
new IdleState(this, stateMachine, BossState.STATE_IDLE, 0.5f, 1f, BossState.STATE_DECIDE),
false);
}
public Boss(Vector2 position)
{
// ...
// AJOUT:
initStateMachine();
}
@Override
public void update(float deltaTime) {
super.update(deltaTime);
// AJOUT:
stateMachine.update(deltaTime);
updateVelocity(deltaTime);
// ...
}
}