I. Qu'est-ce que MonoGame ?▲
MonoGame est une implémentation Open Source du framework Microsoft XNA 4. Le but de cette implémentation est d'offrir la possibilité aux développeurs Xbox 360, Windows et Windows Phone, de porter leurs jeux sur iOS, Android, Mac OS X, Linux et Windows store. D'autres plates-formes seront supportées plus tard comme PlayStation.
Site officiel : http://monogame.codeplex.com/
II. Prérequis▲
Vu que nous allons utiliser C# autant vous dire (si ce n'est déjà fait) d'installer Visual Studio Express 2013 pour Windows Desktop (version utilisée pour cette série de tutoriels) :
http://www.microsoft.com/visualstudio/fra/downloads#d-2013-express
Il vous faudra aussi… MonoGame, bien sûr ! Je vous donne ci-dessous le lien de téléchargement de la dernière version empaquetée pour Windows qui prend en charge les templates pour VS2013 et antérieurs, il vous suffit de cliquer sur le lien dans la page :
http://build.monogame.net/job/develop-win/lastSuccessfulBuild/artifact/Installers/Windows/
Il vous faudra également de bonnes bases en C#, si ce n'est pas le cas, lisez l'article Introduction au langage C#.
Nous pouvons commencer… Je sens que vous avez hâte !
III. Création de notre projet▲
Lancez donc Visual Studio et créez un nouveau projet C# MonoGame de type MonoGame Windows Project et nommez-le « Pacman »… Non, nous n'allons pas faire le jeu en lui-même, mais cela sera notre terrain d'entraînement, car c'est un jeu relativement simple et facile d'accès. Tous les tutoriels de cette série seront donc basés sur ce thème.
Une fois validé, Visual Studio vous génère le squelette du projet (encore heureux). Vous disposez d'une première classe nommée Game1 (Game1.cs), renommez-la en MyPacman.cs que ce soit un peu plus attrayant et explicite ! Au message, cliquez sur Oui et tout le code sera adapté à ce changement ! On dispose donc d'une classe prête à l'emploi avec son constructeur par défaut ainsi que les méthodes suivantes :
- Initialize : sert à l'initialisation de variables et autres paramètres ;
- LoadContent : c'est ici par exemple que nous allons charger les images ;
- UnloadContent : partie dédiée au déchargement des images et autres ressources non managées par la plate-forme .Net donc, les ressources qui ne se trouvent pas dans le dossier Content du projet. Nous verrons l'utilité de ce dossier plus loin ;
- Update : ici viendra tout le code pour mettre à jour l'état du monde avec les nouvelles coordonnées par exemple de notre personnage et de ses ennemis et gérer les collisions entre autres ;
- Draw : nous appellerons ici nos fonctions de dessin à proprement parler donc l'affichage aux nouvelles coordonnées de nos images, du moins pour celles qui sont en mouvement.
Avec ceci nous avons également le fichier Program.cs (comme d'habitude en C#) où se trouve le point d'entrée du programme et dans lequel se trouve déjà la création d'une instance de notre classe MyPacman ! Vous pouvez essayer de lancer le programme, vous obtiendrez normalement une fenêtre avec un fond bleu !
IV. Quelques préparatifs▲
Avant toute chose, il convient de préparer le terrain. Nous avons besoin d'une taille pour notre fenêtre, adaptée par rapport à l'image de fond qui représentera le monde dans lequel notre héros évoluera. Il nous faut aussi des classes qui représenteront les différentes parties du jeu, scène, héros, monstres. Pour cela, nous allons créer différentes classes.
IV-A. La taille de la fenêtre▲
Je vous préviens tout de suite, ici les sprites (images) sont minuscules, car je les ai récupérés sur le site The Sprites Ressources. Ce site propose des sprites d'origine des anciens jeux essentiellement issus des consoles comme GameBoy, SNES, etc.
Donc, dans notre classe MyPacman, après les déclarations suivantes :
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Insérez ces lignes qui sont des constantes qui vont nous permettre de définir la taille de notre fenêtre :
public
const
int
WINDOW_WIDTH =
224
;
public
const
int
WINDOW_HEIGHT =
248
;
Ensuite, dans le constructeur, après les initialisations de base, ajoutez ces deux lignes :
graphics.
PreferredBackBufferWidth =
WINDOW_WIDTH;
graphics.
PreferredBackBufferHeight =
WINDOW_HEIGHT;
Ajoutez ces mêmes lignes dans la méthode Update juste après le commentaire. Les commentaires de type :
// TODO: Add your update logic here
vous montrent l'endroit où vous devez/pouvez ajouter votre code. La fenêtre devrait maintenant ressembler à ceci :
IV-B. La classe de base de nos objets▲
Il nous faut une classe mère pour nos objets. Tous les objets ont les mêmes propriétés de base à savoir la taille, la position ainsi que la faculté de se dessiner eux-mêmes. Faites un clic droit sur le nom du projet dans l'explorateur de solutions puis, ajoutez un nouveau dossier que nous appellerons Core à l'intérieur duquel, vous allez ajouter une nouvelle classe GameObject :
Je vais rester très simple dans le code (ou essayer). Commençons par importer la base du Framework dans cette nouvelle classe :
using
Microsoft.
Xna.
Framework;
using
Microsoft.
Xna.
Framework.
Graphics;
Vous pourrez remarquer qu'effectivement, MonoGame est une implémentation de XNA, on ne peut même pas le cacher, car on utilise l'espace de noms de XNA, ce qui permet d'avoir les codes XNA compatibles ! Puis dans la classe elle-même cette fois-ci, qu'est-ce qu'il nous faut ? Comme dit plus haut, tous les objets ont la même base à savoir… la position et l'image à afficher :
public
Vector2 Position;
public
Texture2D Texture;
Pour finir, il nous faut une méthode de dessin :
public
void
Draw
(
SpriteBatch spriteBatch)
{
spriteBatch.
Draw
(
Texture,
Position,
Color.
White);
}
Méthode dans laquelle nous appelons la méthode Draw de notre argument. Le dernier argument qu'on passe est une couleur qui permet de modifier la teinture de l'image. Nous utilisons ici la couleur blanche pour ne pas modifier notre image. Vous vous demandiez sûrement à quoi peut donc servir cet objet SpriteBatch ! Maintenant vous le savez, il permet de dessiner à l'écran des textures, 2D dans notre cas.
IV-C. Les autres objets▲
Maintenant que nous disposons de notre classe de base, nous pouvons ajouter les différentes classes enfants dont nous avons besoin, à savoir : le monde, le héros Pacman et ses ennemis. Toujours dans le dossier Core, ajoutez les nouvelles classes nommées World, Player, Enemies (un fichier par classe) et faites-les hériter de notre classe GameObject.
V. Dessine-moi… un monde▲
Nous y sommes, nous allons ajouter notre image de fond où notre héros évoluera ! Dans notre classe MyPacman, dans la partie des importations, ajoutez notre espace de noms Pacman.Core :
using
Pacman.
Core;
Juste après les deux constantes que nous avons ajoutées au début, il nous faut déclarer une variable pour notre monde donc :
World world;
Puis dans la méthode Initialize juste après le commentaire :
world =
new
World
(
);
Ce qui nous crée une instance de notre image de fond. Maintenant, il faut ajouter l'image au projet, téléchargez l'archive où se trouvent les quelques images du projet : https://franckh.developpez.com/tutoriels/csharp/monogame/part-I/images/Pacman-images.zip
Décompressez-la puis faites un glisser-déposer de l'image world.png vers le dossier Content de notre projet dans l'explorateur de solutions. Le dossier Content permet d'avoir un accès direct à nos ressources dans le projet. Les ressources ainsi déposées sont automatiquement copiées dans le dossier physique du même nom dans le dossier de notre projet. En de plus de cela, le dossier est couplé au ContentManager qui va grandement nous faciliter les choses, du chargement des images à leur libération qui elle, est automatique !
L'exclusion d'une ressource du projet (menu contextuel avec le clic droit) ne la supprimera pas physiquement, elle est juste déréférencée de notre projet. Pour la supprimer définitivement, utilisez la commande Supprimer plus bas dans le menu contextuel.
Une chose importante à faire à l'ajout de chaque ressource : cliquez sur l'image et dans la fenêtre de propriétés, sur la propriété Copier dans le répertoire de sortie, mettez à la valeur Copier si plus récent. Sans cela, l'image ne sera pas copiée dans le répertoire de l'exécutable et il y aura une erreur de chargement !
Nous y sommes presque, il ne reste plus que deux étapes ! L'avant-dernière consiste à charger notre image fraîchement ajoutée à notre projet. Dans la méthode LoadContent, juste après le commentaire qui nous est destiné, ajoutez ces deux lignes :
world.
Texture =
Content.
Load<
Texture2D>(
"world"
);
world.
Position =
new
Vector2
(
0
,
0
);
Vous aurez pu remarquer que nous appelons notre image uniquement par son nom ! Oui étant donné que c'est le ContentManager qui se charge de la gestion des ressources, il ne nous est pas nécessaire d'utiliser l'extension. Cela ajoute cependant une contrainte, chaque fichier doit avoir un nom unique !
La seconde ligne nous permet de positionner l'image. Comme dans toutes les bibliothèques graphiques, ces coordonnées correspondent au coin supérieur gauche. Il ne nous reste plus qu'à dessiner notre image. Dans la méthode Draw, ajoutez ces lignes :
spriteBatch.
Begin
(
);
world.
Draw
(
spriteBatch);
spriteBatch.
End
(
);
Je pense que vous vous demandez à quoi servent les appels qui entourent notre appel à la méthode Draw de notre objet ! Je ne vais pas trop rentrer dans les détails. Ce qu'il faut savoir, c'est que c'est ici que MonoGame va lancer un traitement qui sera spécifique à la signature de la méthode Begin utilisée, car il existe différentes signatures. Vous pouvez vous rendre sur le site de Microsoft pour plus d'informations à ce sujet SpriteBatch.Begin Méthode
Dans cet appel, ce qu'il faut savoir, c'est que le rendu de nos images ne sera effectif qu'à l'appel de la méthode End. Une fois appelée, cette méthode va paramétrer la carte graphique et appellera chaque méthode Draw dans leur ordre d'apparition dans le code.
Lancez votre projet, vous devriez vous retrouver avec un écran similaire au mien :
VI. Et Pacman alors ?▲
On y vient, un monde vide ne sert à rien, je le sais ! Nous allons donc maintenant nous préoccuper un peu de notre héros. Dans le dossier Content, ajoutez le jeu de sprites le représentant !
À partir d'ici, cela va se corser un petit peu. Lorsque nous regardons notre image, on voit qu'il y a toute une série d'images à la suite ! Ce que nous allons devoir faire, c'est dessiner la bonne portion par rapport à la direction dans laquelle le personnage se déplace et également afficher les diverses images intermédiaires qui permettront de créer une petite animation. Modifions la classe GameObject.
Commençons par ajouter quelques variables :
// Rectangle permettant de définir la zone de l'image à afficher
public
Rectangle Source;
// Durée depuis laquelle l'image est à l'écran
public
float
time;
// Durée de visibilité d'une image
public
float
frameTime =
0
.
1f
;
// Indice de l'image en cours
public
int
frameIndex;
Ensuite, pour nous simplifier la tâche, nous allons définir en avance l'indice de chaque image. Nous allons faire ceci avec une énumération tout simplement. Il faut juste prendre le temps de regarder l'image de notre héros :
Nous avons la direction droite à l'indice 0 et 1, etc. Deux images par direction, ce qui nous donne le code suivant (il en sera de même pour les ennemis, ce qui nous arrange bien) :
public
enum
framesIndex
{
RIGHT_1 =
0
,
RIGHT_2 =
1
,
BOTTOM_1 =
2
,
BOTTOM_2 =
3
,
LEFT_1 =
4
,
LEFT_2 =
5
,
TOP_1 =
6
,
TOP_2 =
7
}
Notez bien le fait que ce n'est pas le même nom que la variable, frame est au pluriel !
Ajoutons ensuite quelques propriétés qui ne possèdent que la méthode Get. Qui dit propriété dit variable privée en plus :
private
int
_totalFrames;
public
int
totalFrames
{
get
{
return
_totalFrames;
}
}
private
int
_frameWidth;
public
int
frameWidth
{
get
{
return
_frameWidth;
}
}
private
int
_frameHeight;
public
int
frameHeight
{
get
{
return
_frameHeight;
}
}
Inutile de les commenter, je suppose, leur nom est assez explicite !
Donnez toujours des noms les plus explicites possible à vos variables, méthodes, propriétés, etc. Le code n'en sera que largement plus lisible et vous éviterez un tas de commentaires inutiles !
Nous allons maintenant ajouter deux constructeurs à notre classe. Jusque-là nous utilisions le constructeur par défaut, mais si on veut pouvoir initialiser nos propriétés par exemple, il nous faut construire nos propres constructeurs :
public
GameObject
(
)
{
}
public
GameObject
(
int
totalAnimationFrames,
int
frameWidth,
int
frameHeight)
{
_totalFrames =
totalAnimationFrames;
_frameWidth =
frameWidth;
_frameHeight =
frameHeight;
}
Pour le moment on reste simple, le constructeur par défaut est juste là pour que Visual Studio ne nous embête pas, car il voudra un constructeur de ce type. Nous appellerons le second constructeur pour pouvoir commencer à régler nos animations. Ajoutons également une méthode DrawAnimation en dessous de Draw :
public
void
DrawAnimation
(
SpriteBatch spriteBatch)
{
}
Nous reviendrons un peu plus tard sur notre méthode DrawAnimation. Il nous faut avant de l'utiliser, obtenir des informations supplémentaires comme, un Rectangle contenant les coordonnées du sprite à afficher. Il faut aussi calculer le temps passé afin de savoir quand changer l'indice de notre sprite permettant de calculer ses coordonnées. Ajoutons alors une méthode UpdateFrame (en dessous de DrawAnimation) :
public
void
UpdateFrame
(
GameTime gameTime)
{
time +=
(
float
)gameTime.
ElapsedGameTime.
TotalSeconds;
while
(
time >
frameTime)
{
frameIndex++;
time =
0f
;
}
if
(
frameIndex >
_totalFrames)
frameIndex =
0
;
Source =
new
Rectangle
(
frameIndex *
frameWidth,
0
,
frameWidth,
frameHeight);
}
Je sais que ça a l'air compliqué, mais il n'en est rien. Cette fonction permet dans un premier temps de calculer le temps passé depuis notre dernière mise à jour de l'affichage de notre sprite. Nous passons ensuite au prochain indice de notre image si nous avons dépassé le temps d'affichage puis, on remet à zéro la variable time pour la réutiliser correctement pour la prochaine mise à jour. Si l'indice dépasse le nombre de sprites dans notre collection, on repasse au premier. Nous calculons ensuite la position du nouveau sprite à afficher en déterminant sa position par rapport à l'indice en cours. Vous voyez, c'est simple comme bonjour !
Revenons maintenant à notre méthode DrawAnimation, nous avons maintenant toutes les informations nécessaires au bon affichage de notre héros. Ajoutons alors la méthode de rendu :
spriteBatch.
Draw
(
Texture,
Position,
Source,
Color.
White);
Avant de pouvoir lancer et même afficher tout cela, il ne faut pas oublier un détail, si vous regardez nos constructeurs de la classe GameObject, en pensant que notre héros est animé, il faut également un constructeur pour notre personnage ! Dans notre classe Player, ajoutons un constructeur avec les mêmes paramètres que le second constructeur de notre classe GameObject. Ce constructeur appellera justement le constructeur parent grâce à l'instruction base :
public
Player
(
int
totalAnimationFrames,
int
frameWidth,
int
frameHeight)
:
base
(
totalAnimationFrames,
frameWidth,
frameHeight)
{
}
Retrouvons-nous maintenant dans la classe MyPacman pour y ajouter la déclaration, l'instanciation et les appels qui vont bien. En dessous de la déclaration de notre monde, ajoutons celle de notre personnage :
Player player;
Puis, dans la méthode Initialize, créons enfin notre héros ! Dans l'ordre des arguments, il possède huit images et fait treize pixels de largeur et de hauteur.
player =
new
Player
(
8
,
13
,
13
);
Viens ensuite le chargement et le positionnement dans la méthode LoadContent :
player.
Texture =
Content.
Load<
Texture2D>(
"pacman"
);
player.
Position =
new
Vector2
(
0
,
109
);
Dans la méthode Update, après la définition de la taille de la surface de jeu :
player.
UpdateFrame
(
gameTime);
Et pour terminer le rendu dans la méthode Draw (bien sûr après l'affichage du monde, sinon il serait en dessous le pauvre):
player.
DrawAnimation
(
spriteBatch);
Vous pouvez d'ores et déjà lancer le programme :
Bon vous me direz… Il a la tête qui tourne le pov' Pacman, hélas oui je vous répondrai ! Il ne sait pas encore où il faut aller ni même comment démarrer correctement et c'est ce que nous allons voir dans le prochain chapitre !
VII. On va bouger, bouger…▲
Bon, notre personnage tourne sur lui-même, c'est pas sympa ça, nous allons donc y remédier. On va devoir travailler sur la modification de quelques classes donc chaque chapitre correspondra à la modification de l'une d'entre elles. Nous commencerons par la classe GameObject puis nous passerons à la classe Player. Nous retournerons sur la classe GameObject pour finir ses modifications et nous terminerons par la classe MyPacman. Je ne vous avais pas dit que j'écrivais ce tutoriel en même temps que je développe l'application ? Maintenant vous le savez ;)
VII-A. Modification de la classe : GameObject▲
Qui dit mouvement dit direction, il nous faut donc des constantes pour nous simplifier la tâche plus tard, ajoutons une énumération :
public
enum
Direction
{
LEFT =
0
,
RIGHT =
1
,
TOP =
2
,
BOTTOM =
3
}
On peut, par la même occasion, ajouter une variable de ce type :
public
Direction direction;
VII-B. Modification de la classe : Player▲
Ajoutons de quoi reconnaître le clavier :
using
Microsoft.
Xna.
Framework.
Input;
Puis ajoutons-y une méthode permettant de gérer les touches du clavier :
public
void
Move
(
KeyboardState state)
{
}
On va également initialiser la position de départ de notre personnage dans son constructeur ainsi que la première image à afficher (si Visual Studio vous embête à cause du type pour notre indice d'image ce n'est pas grave, nous procéderons à d'autres modifications plus loin) :
direction =
Direction.
RIGHT;
frameIndex =
framesIndex.
RIGHT_1;
Que devons-nous savoir quant aux mouvements de notre personnage ? Si on appuie sur la touche Z, alors il monte donc, Q pour la gauche, S pour le bas et D pour la droite ! N'oublions pas non plus de renseigner notre variable direction ! On va pour le moment bouger de un pixel. Nous pouvons donc implémenter notre méthode Move comme suit :
public
void
Move
(
KeyboardState state)
{
if
(
state.
IsKeyDown
(
Keys.
Z))
{
direction =
Direction.
TOP;
Position.
Y -=
1
;
}
if
(
state.
IsKeyDown
(
Keys.
Q))
{
direction =
Direction.
LEFT;
Position.
X -=
1
;
}
if
(
state.
IsKeyDown
(
Keys.
S))
{
direction =
Direction.
BOTTOM;
Position.
Y +=
1
;
}
if
(
state.
IsKeyDown
(
Keys.
D))
{
direction =
Direction.
RIGHT;
Position.
X +=
1
;
}
}
Vous savez maintenant comment gérer le clavier !
VII-C. Modification de la classe : GameObject, la suite▲
Il nous faut finir les modifications de cette classe. Nous allons enfin utiliser nos constantes framesIndex mais avant cela, il va falloir changer le type de notre variable frameIndex qui est de type int,en type framesIndex.
Ensuite, supprimez l'incrémentation de notre frameIndex, oui oui, on le vire, y'en a marre de tourner en rond ! Ce que nous devons faire, c'est utiliser les bonnes images suivant l'orientation de notre personnage, orientation que nous définissons déjà au chargement de notre classe Player. Nous allons donc mettre un switch en place où chaque case correspond à ? Une direction !
switch
(
direction)
{
case
Direction.
TOP:
if
(
frameIndex ==
framesIndex.
TOP_1)
frameIndex =
framesIndex.
TOP_2;
else
frameIndex =
framesIndex.
TOP_1;
break
;
case
Direction.
LEFT:
if
(
frameIndex ==
framesIndex.
LEFT_1)
frameIndex =
framesIndex.
LEFT_2;
else
frameIndex =
framesIndex.
LEFT_1;
break
;
case
Direction.
BOTTOM:
if
(
frameIndex ==
framesIndex.
BOTTOM_1)
frameIndex =
framesIndex.
BOTTOM_2;
else
frameIndex =
framesIndex.
BOTTOM_1;
break
;
case
Direction.
RIGHT:
if
(
frameIndex ==
framesIndex.
RIGHT_1)
frameIndex =
framesIndex.
RIGHT_2;
else
frameIndex =
framesIndex.
RIGHT_1;
break
;
}
Ce que nous faisons ici, c'est déterminer à quel indice d'image correspond le frameIndex et nous ajustons la valeur en fonction du résultat ! Ensuite il faut modifier l'appel suivant de cette façon :
Source =
new
Rectangle (
(
int
)frameIndex *
frameWidth,
0
,
frameWidth,
frameHeight);
Nous avons juste ajouté un cast pour la position X sur notre série d'images. Normal, nous avons changé le type de notre variable ! Passons à la dernière classe à modifier !
VII-D. Modification de la classe : MyPacman▲
Du vite fait dans cette classe. Il y a juste à ajouter dans la méthode Update, avant notre appel à player.UpdateFrame, notre méthode Move. Sans oublier de récupérer l'état du clavier et de le passer en paramètre :
player.
Move
(
Keyboard.
GetState
(
));
Vous pouvez lancer le programme ! Miracle ! Ça bouge ! Notre personnage a pris vie, enfin ! Bien sûr, il bouge partout, même là où il ne peut normalement pas aller, mais ça, ce sera pour une prochaine fois !
VIII. Résumé▲
Oui je sais, c'est la fin, mais que de cette première partie !
Nous avons donc vu dans cette première partie, comment installer les outils nécessaires au développement de jeux avec MonoGame en C# sur Windows. Nous avons également vu comment, créer des classes adaptées pour ce type de projet, les informations dont nous avons besoin pour avoir un projet un minimum opérationnel, comment afficher une image fixe et comment également, créer et afficher une animation et pour finir, nous avons vu comment gérer les déplacements !
Cela fait déjà pas mal de choses pour un bon début !
IX. Code source▲
Vous pouvez télécharger ci-après, le code source de la solution complète par rapport à cette première partie du tutoriel : franckh.developpez.com/tutoriels/csharp/monogame/part-I/fichiers/Pacman.zip
X. Remerciements▲
Un grand merci à LittleWhite et à ClaudeLELOUP pour la relecture et correction de l'article ainsi qu'à CyaNnOrangehead pour ses remarques.