Moteur de jeu
Approche générale
1. Présentation
Comme présenté rapidement au chapitre Concevoir une application d’envergure, l’approche générale suivie dans cet ouvrage est une version modifiée de l’approche « Modèle-Vue-Contrôleur (MVC) » :
En guise de modèle, on trouve l’état du jeu, présenté dans le chapitre Représenter l’état du jeu. Contrairement à l’approche MVC, l’état du jeu dans l’approche proposée est uniquement un lieu de stockage : il n’y a pas de procédure de validation des modifications. Dans l’exemple du jeu Pacman présenté jusqu’à maintenant, les attributs de la classe PlayGameMode forment l’état du jeu. Par exemple, l’attribut level contient la nature des cellules du monde. En guise de vue, on trouve l’interface utilisateur, présentée dans le chapitre Interface utilisateur. Enfin, en guide de contrôleur, on trouve le moteur de règles, incarné par la méthode update() dans l’exemple de jeu Pacman. Contrairement à l’approche MVC, c’est le moteur de règles qui assure la validation des modifications de l’état du jeu.
En termes d’interaction entre ces trois acteurs, la différence majeure réside dans le caractère passif de l’état du jeu. Lorsque l’utilisateur effectue...
Synchronisation entre état et interface utilisateur
L’objectif de cette section est de synchroniser les données de l’état du jeu conçu dans le chapitre Représenter l’état du jeu avec l’interface utilisateur présentée au chapitre Interface utilisateur.
Avant de démarrer, voici un récapitulatif des éléments fabriqués jusqu’à présent, avec un peu de réorganisation.
Tout d’abord, les classes principales sont conservées dans le package principal et séparées des classes de menu :
Toutes les classes de menu sont placées dans le package menu :
La façade pour l’interface utilisateur est conservée et placée dans le package gui :
L’implantation avec la bibliothèque AWT est également conservée, et placée dans le package gui.awt. Il est possible d’utiliser une autre implantation - toute la suite ne dépend plus d’une implantation particulière, dès lors que les implantations des nouvelles fonctionnalités de la façade sont ajoutées.
L’état du jeu Pacman, présenté au chapitre Représenter l’état du jeu, est réorganisé en deux parties : une première partie avec les conteneurs, placée dans le package state (ici sans la classe WorldIterator pour plus de clarté) :
La deuxième partie, avec les éléments, est placée dans le package state.element :
1. Dessiner l’état du jeu
a. Initialiser le monde (Patron Fabrique Abstraite)
Pour pouvoir dessiner un état du jeu, il faut tout d’abord le remplir avec des données qui représentent un niveau de Pacman. Dans les exemples du chapitre Interface utilisateur, un simple tableau natif a été utilisé :
static final int[][] level = new int[][] {
{ 15,11,11,11,11,11,11,11,16 },
{ 12,5,3,3,3,3,3,3,12 },
{ 12,3,15,11,11,11,16,3,12 },
{ 14,3,13,11,11,11,14,3,13 },
{ 3,3,3,3,3,3,3,3,3 },
{ 11,11,11,11,11,11,11,11,11 }
};
Ce tableau est très bien pour présenter...
Règles du jeu
1. Définir les règles du jeu
À présent que l’état du jeu et le moteur de rendu sont connectés, il ne reste qu’à concevoir un moteur de règles, puis à connecter l’ensemble, pour former une solution complète. Avant de commencer la conception de ce moteur, il est fortement conseillé, tout comme pour l’état, de coucher sur le papier les différentes règles du jeu. Cette étape peut être faite en même temps que la définition de l’état - elle est proposée plus tard dans cet ouvrage pour éviter de présenter trop de notions dès le début.
Idéalement, cette description des règles doit être précise, sans pour autant trop ressembler à un algorithme. Des choix très importants sont faits à ce niveau, il est impératif de prendre le temps de la réflexion. Sur la forme, les mécaniques liées aux commandes doivent apparaître. Par exemple, le type d’ordre que peut donner un joueur ou une IA. Cela peut être la commande d’un déplacement d’un personnage, ou la modification des propriétés d’un bâtiment, etc. Attention, l’origine de ces commandes ne présente pas d’intérêt ici. Par exemple, le fait qu’il faille presser une touche ou cliquer à un endroit précis n’est pas considéré lors de cette étape. Ces origines sont ignorées, et on fait l’hypothèse que ces commandes seront produites d’une manière ou d’une autre, via l’interface utilisateur, une IA ou le réseau.
Il existe aussi des événements qui n’ont pas d’origine extérieure. Par exemple, dans un jeu avec des mines d’or, de l’or est ajouté au trésor à chaque fin de tour. Cette opération n’est pas nécessairement déclenchée par l’utilisateur, qui n’a pas forcément besoin de cliquer sur chaque mine pour récolter l’or miné. Pour ces cas, on considère des commandes passives, déclenchées de manière systématique à chaque mise à jour du jeu, sans intervention d’un joueur...
Fonctionnalités supplémentaires
Le modèle complet qui réunit état du jeu, interface utilisateur et moteur de règles permet de facilement implanter diverses fonctionnalités, comme l’intelligence artificielle ou le jeu en réseau présentés dans les chapitres suivants. Dans cette section, quelques exemples sont proposés.
1. Paramétrer une partie (Patron Constructeur)
Le menu créé précédemment dans l’interface utilisateur n’utilisait pas les choix pour paramétrer une partie, comme choisir le personnage joué ou le niveau de difficulté. Une approche simple consiste à fabriquer dès le début du menu une partie, via une instance de la classe PlayGameMode, puis à modifier ses paramètres en fonction des choix dans le menu. Cette approche pose un certain nombre de problèmes. Ceux-ci sont liés à la nécessité inévitable de séparer les problématiques. Avec une approche simple, on mélange la collecte des paramètres et la création d’une partie. En outre, dans le cas du jeu Pacman la création d’une partie ne dépend que d’une seule classe. Il existe cependant d’autres cas où plusieurs types de parties sont possibles, et donc autant de classes. Il faudrait alors créer et détruire les instances de ces classes en fonction du parcours de l’utilisateur dans le menu.
Patron Constructeur
Pour résoudre ces problèmes, il suffit d’utiliser le patron Constructeur (Builder Pattern), qui place la collecte des paramètres d’un côté, et la création des objets de l’autre.
Ce patron, dans sa forme la plus courante, se présente de la manière suivante :
La classe Director est l’utilisateur du patron : elle appelle les différentes méthodes de l’interface IBuilder dans le but d’obtenir une instance de la classe Product. Les différentes méthodes peuvent avoir ou ne pas avoir un ordre appel précis, sauf pour la méthode getProduct() qui renvoie l’objet final. Ces méthodes peuvent être des étapes de construction, comme ajouterFondations(), ajouterMurs() et ajouterToit() pour une maison. Elles peuvent également...
Solutions des exercices
1. Exercice 2.4.1 : Fabriquer une galaxie
Une solution est proposée dans le dossier « examples/chap04/stellaris » des packages sources, ainsi que des tests unitaires dans le même dossier dans les packages de tests.
La non-dépendance aux types de planètes repose sur l’utilisation du patron Fabrique Abstraite pour créer celles-ci :
La fabrique de planète suit le patron classique, sachant que le nom de la planète doit être fourni lors de sa construction. La méthode create() de la fabrique a donc deux arguments : le type et le nom de la planète.
public class PlanetFactory {
private Map<String, PlanetCreator> creators = new HashMap();
public void registerCreator(String type, PlanetCreator creator) {
creators.put(type, creator);
}
public void unregisterCreator(String type) {
creators.remove(type);
}
public Planet create(String type,String name) {
return creators.get(type).create(name);
}
}
La classe GalaxyLoader est définie pour charger une galaxie depuis une liste de String. Elle ne dépend pas des classes filles de Planet. Elle contient une fabrique par défaut, mais qui peut être modifiée lors de l’exécution.
public class GalaxyLoader {
private final String[] data;
private int index;
private PlanetFactory factory = new PlanetFactory();
public GalaxyLoader(String[] data) {
this.data = data;
factory.registerCreator("Habitable", new PlanetCreator() {
@Override
public Planet create(String name) {
return new Habitable(name);
}
});
factory.registerCreator("Gaseous"...