Feuilles de root

Logiciels libres, programmation et économie

Accueil » Programmation » Patrons de conceptions Design patterns » Comment gérer simplement les états d'un jeu vidéo ?

Comment gérer simplement les états d'un jeu vidéo ?

La gestion des états du jeu est souvent compliquée.

Commençons par un rappel sur la notion d'état dans un jeu.

Que se passe-t-il lorsque vous démarrez un jeu ? Vous verrez probablement une séquence d'introduction suvi du menu principal. Vous effectuez des réglages (clavier, résolution), choisissez le niveau ou le scénario puis vous commencez à jouer. Si vous êtes interrompu, vous mettez le jeu en pause pour reprendre la partie ensuite.

Au cours d'une partie le jeu peut prendre différents états : le menu principal, un sous-menu, la carte du monde, une carte locale, une phase de combat, un menu de dialogue, l'écran de pause, l'écran de scores.

La manière de gérer les états la plus répandue est d'utiliser une pile. Les différents états sont empilés et le moteur de jeu met à jour et met à jour et affiche l'état en tête de pile.

Je présente ici un système simple pour gérer les états du jeu.

Cet article se base sur la solution proposée ici et présente une implémentation en Pascal.

Un état peut être mis à jour et affiché. On peut définir une interface :

IGameState = interface
    procedure Update(const ADeltaTime : Integer);
    procedure Render(AScreen : PSDL_Surface);
end;

Nous allons définir des états concrets comme le menu ou le jeu, qui implémentent cette interface. Apportons à present une lègère amélioration en faisant de la méthode Update une fonction :

IGameState = interface
    function Update(const ADeltaTime : Integer) : IGameState;
    procedure Render(AScreen : PSDL_Surface);
end;

En effet, il faut pouvoir assurer les transitions entre états. A présent, la fonction Update retourne l'état suivant. S'il n'y a pas de changement, elle retourne l'objet courant (Self).

TIntroduction = class(TInterfacedObject, IGameState)
    function Update(const ADeltaTime : Integer) : IGameState;
    procedure Render(AScreen : PSDL_Surface);
end;
TMenu = class(TInterfacedObject, IGameState)
    function Update(const ADeltaTime : Integer) : IGameState;
    procedure Render(AScreen : PSDL_Surface);
end;
function TMenu.Update(const ADeltaTime : Integer) : IGameState;
begin
if NewGameButton.Clicked then
    Result := TGame.Create
else
    Result := Self;
end;

Désormais, la gestion des états du jeu devient extrếmement simple et ne nécessite pas de gestionnaire d'état ni de pile. La boucle de jeu ressemble à :

FCurrentState := TIntroduction.Create;
while IsRunning do
begin
    HandleInput;
    FCurrentState := FCurrentState.Update(CalculateDeltaTime);
    FCurrentState.Render(AScreen);
    PresentAndClear;
end;

Il est parfois nécessaire de rajouter un état provisoirement puis reprendre l’état précédent sans aucune perte de donnée. Prenons l'état de pause. Une fois la pause terminée, vous voulez réactiver le jeu et reprendre la partie. Habituellement, une pile est préconisée.

Ici on se débarrasse de la pile. L'état de pause va prendre l'état précédent en paramètre dans son constructeur, qui est stocké et retourné au lieu de l'état de pause lui-même lorsque la méthode update détecte que le jeu doit reprendre.

TPause = class(TInterfacedObject, IGameState)
private
    FPreviousState : IGameState;
public
    constructor Create(APreviousState : IGameState);
    function Update(const ADeltaTime : Integer) : IGameState;
    procedure Render(AScreen : PSDL_Surface);
    destructor Destroy; override;
end;
constructor TPause.Create(APreviousState : IGameState);
begin
    FPreviousState := APreviousState;
end;

function TPause.Update(const ADeltaTime : Integer) : IGameState;
begin
    if ResumeButton.Clicked then
        Result := FPreviousState
    else
        Result := Self;
end;

destructor TPause.Destroy; override;
begin
    FPreviousState := nil;
end;

Comme en Free Pascal, les interfaces sont gérées via un compteur de référence, vous n'avez pas à vous préoccuper de la libération des objets.