HAKUROU L'amour est dans le code

Créer un jeu HTML5 - part 1

Créer un jeu HTML5 - part 1 <br>

Le but de cette série de billets est de réaliser un petit jeu en HTML5 et de voir ensemble les différents mécanismes permettant de rendre un jeu jouable.

Il est souvent conseillé aux personnes qui veulent créer un jeu de commencer petit comme par exemple un Tetris ou un pacman. En effet les mécanismes sont relativement simples à mettre en oeuvre et le temps de développement requis n'est pas très long, ce qui représente un bon début pour se faire la main et apprendre des choses.

Pour ce tutoriel nous allons nous baser sur la célèbre pastille jaune de Namco: Pacman.

Pour cette version, nous allons bâtir de zéro un système de jeu qui sépare le game logic et la couche graphique, ce qui nous permettra si on le souhaite, de changer complèment l'aspect graphique et de migrer de la 2D à la 3D avec WebGL sans toucher la logique de jeu.

Personnellement j'utilise DJS en surcouche JavaScript, vous pouvez de votre côté utiliser ce que vous souhaitez (TypeScript, CoffeeScript, etc...), il faudra juste adapter le code, ça ne sera pas compliqué, la logique reste la même.

 

Le cahier des charges

Avant de commencer quoi que ce soit, il est impératif de définir des règles claires afin de savoir ce que l'on veut faire.

Nous allons définir les principales règles suivantes:

  • pour cette version, le jeu doit être en 2D basé sur des tuiles (tiles)
  • tenir compte de la séparation du game logic et de la couche graphique
  • le jeu se déroule sur une grille de 20x14, soit 20 cases en largeur et 14 en hauteur
  • pouvoir diriger pacman avec les flèches du clavier dans les 4 directions
  • pacman doit manger toutes les billes pour passer au niveau suivant (4 niveaux en tout)
  • les ennemis sont 4 fantômes qui se déplacent de façon aléatoire pour simplifier la mise en place du jeu
  • les collisions avec les fantômes font perdre 1 vie sur les 4 de base, si pacman n'a plus de vie, la partie est fini
  • les powerup sont là pour permettre à pacman de manger les fantômes durant un temps définit à 5s, les fantômes redémarre de leur point d'origine

 

Graphismes

 Pour les graphismes, rien de très compliqué, sous Gimp on créé deux tilesets, un pour les graphiques du jeu, l'autre pour le texte.

 

sprites pacman
font pacman
Tile: 16x16 Tile: 8x8

 

On aurait pu tout mettre dans un seul, le choix n'a pas vraiment d'importance

 

Les bases du système

Maintenant on va commencer le code pour les bases du système, afin d'avoir au moins un petit quelque chose à l'écran.

N'oubliez pas que j'utilise DJS, et que j'utiliserais abondamment de namespace, class, d'héritage et import. Mes fichiers JS sous DJS ont l'extension .djs. Je n'utilise pas de raccourcis sur les namespaces donc ne prennez pas peur en voyant la longueur de certains.

Créez un dossier dans lequel nous pourrons coder notre jeux, n'oubliez pas d'y glisser les tilesets, nous en auront besoin dans la partie suivante.

Tout d'abord, il nous faut le fichier HTML suivant:

index.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf8">
		<title>PacmanJs</title>
	</head>
	<body>
		<canvas id="main-canvas-game" width="640" height="480"></canvas>
		<script src="hakurou/pacmanjs/main.js" ></script>
	</body>
</html>

 

L'inclusion de l'armorce fait référence à hakurou/pacmanjs/main.js, c'est de là que tout va partir:

hakurou/pacmanjs/main.djs

(function(){
	
	"use strict";
	
	import "core/Game.djs";
	import "gfx/canvas/EngineCanvas.djs";
	
	var game = new hakurou.pacmanjs.core.Game(
		// Moteur de rendu via canvas avec context 2D
		new hakurou.pacmanjs.gfx.canvas.EngineCanvas("main-canvas-game")
	);
	
	game.run();
})();

 

Le principe est simple, on instancie le jeu en lui demandant d'utiliser une sortie graphique spécifique, dans le cas présent c'est le context 2D de la balise canvas. C'est ici que nous pouvons demander au jeu de s'afficher via WebGL, mais ce n'est pas pour tout de suite.

Game.djs est la classe qui va se charger le l'initialisation du moteur ainsi que la boucle principale de jeu.
EngineCanvas.djs est la classe héritée de Engine qui prépare le menu principale et la gestion des niveaux, elle représente la partie "graphique" du jeu.

 

hakurou/pacmanjs/core/Game.djs

import "Events.djs";

namespace hakurou.pacmanjs.core
{
	class Game
	{
		engine = null;
		events = null;
		
		construct(engine)
		{
			this.engine = engine;
			this.prepareRAF();
			this.engine.setGame(this);
		}
		
		run()
		{
			this.events = new  hakurou.pacmanjs.core.Events();
			this.appLoop();
		}
		
		appLoop()
		{
			var self = this;
			window.requestAnimationFrame(function(){
				self.appLoop();
				self.render();
			});
		}
		
		render()
		{
			this.engine.render(this.events);
		}
		
		prepareRAF()
		{
			var self = this;
			window.requestAnimationFrame = (function(){
				return window.requestAnimationFrame
					|| window.mozRequestAnimationFrame 
					|| window.webkitRequestAnimationFrame 
					|| window.msRequestAnimationFrame
					|| function(callback){setTimeout(callback, 1000 / 60);};
			})();
		}
	}
}

 

Une fois cette classe instanciée, elle va créer une boucle de jeu utilisant la fonction requestAnimationFrame, cadencée à 60fps.
Dans cette boucle, Game va demander le rendu du moteur en lui injectant l'instance de Events, qui s'occupe des évènements utilisateur comme le clavier.

Events est la classe que l'on va intéroger pour savoir qu'elle touche a été appuyée, elle est définit comme ci-dessous:

hakurou/pacmanjs/core/Events.djs

import "../misc/Tools.djs";

namespace hakurou.pacmanjs.core
{
	class Events
	{
		key = {};
		
		keyCodes = {
			39:		"right",
			37:		"left",
			38:		"up",
			40:		"down",
			32:		"space",
			13:		"enter",
			27:		"escape"
		};
		
		construct()
		{
			this.defineKeys();
			this.bindEvents();
		}
		
		defineKeys()
		{
			for(var i in this.keyCodes)
			{
				this.key[this.keyCodes[i]] = false;
			}
		}
		
		bindEvents()
		{
			var self = this;
			hakurou.pacmanjs.misc.Tools.addEvent("keydown", document, function(event){
				self.setKeyCodeStatus(event.keyCode, true);
			});
			
			hakurou.pacmanjs.misc.Tools.addEvent("keyup", document, function(event){
				self.setKeyCodeStatus(event.keyCode, false);
			});
		}
		
		setKeyCodeStatus(keycode, active)
		{
			if(typeof this.keyCodes[keycode] != "undefined")
				this.key[this.keyCodes[keycode]] = active;
		}
	}
}

 

Rien de très sorcier, on bind les listeners adéquates et on récupère les statuts des touches si elles sont active ou non.
J'ai sorti la méthode addEvent dans une classe utilitaire Tools car elle n'est pas forcément propre à Events.

hakurou/pacmanjs/misc/Tools.djs

namespace hakurou.pacmanjs.misc
{
	class Tools
	{
		static addEvent(eventName, node, callback)
		{
			if(node.addEventListener)
				node.addEventListener(eventName, callback);
			else 
				node.attachEvent("on" + eventName, callback);	
		}
	}
}

 

Avant de montrer EngineCanvas, il nous faut faire sa classe parente, qui pose les bases de son fonctionnement:

hakurou/pacmanjs/core/Engine.djs

namespace hakurou.pacmanjs.core
{
	class Engine
	{
		game = null;
		currentStage = null;
		
		construct()
		{

		}
		
		setGame(game)
		{
			this.game = game;
		}
		
		render(e)
		{
			if(this.currentStage != null)
			{
				this.currentStage.render(e);
			}
		}
		
		setStage(stage)
		{
			this.currentStage = stage;
		}
		
	}
}

 

Engine prend en charge le niveau courant et la méthode render demandera à chaque tour de boucle principale le rendu de celui-ci en lui passant l'instance de Events.

La classe graphique de Engine se compose comme ceci:

hakurou/pacmanjs/gfx/canvas/EngineCanvas.djs

import "../../core/Engine.djs";
import "core/Canvas.djs";

namespace hakurou.pacmanjs.gfx.canvas
{
	class EngineCanvas extends hakurou.pacmanjs.core.Engine 
	{
		canvas = null;
		
		construct(canvasName)
		{
			this.canvas = new hakurou.pacmanjs.gfx.canvas.core.Canvas(canvasName);

		}
		
		render(e)
		{
			this.canvas.clear();
			super.render(e);
		}
	}
}

 

Pour l'instant son rôle est réduit mais elle va s'étoffer au fur et à mesure du jeu.
EngineCanvas va initialiser canvas et à chaque boucle on va raffraichir l'interface.

J'ai sorti l'initialisation de canvas dans une classe prévue à cette effet pour mieux séparer les rôles, ça rend également le code plus lisible.

hakurou/pacmanjs/gfx/canvas/core/Canvas.djs

namespace hakurou.pacmanjs.gfx.canvas.core
{
	class Canvas
	{
		canvas = null;
		context = null;
		width = null;
		height = null;
		
		construct(canvasName)
		{
			this.initCanvas(canvasName);
		}
		
		initCanvas(canvasName)
		{
			this.canvas = document.getElementById(canvasName);
			this.context = this.canvas.getContext("2d");
			this.width = this.canvas.width;
			this.height = this.canvas.height;
			
			this.context.imageSmoothingEnabled = false;
			this.context.mozImageSmoothingEnabled = false;
		}
		
		clear()
		{
			this.context.save();
			this.context.fillStyle = "rgb(0, 0, 0)";
			this.context.fillRect(0, 0, this.width, this.height);
			this.context.restore();
		}
	}
}

 

Nous récupérons le contexte graphique et, chose importante, on désactive le smoothing des images, en effet pour ce jeu il est plus sympa d'avoir des pixels nets que d'avoir un lissage de texture qui gacherait l'aspect kitch du jeu.

 

Normalement si vous avez tout bien fait, vous devriez avoir une fenête noire à l'écran.

Si vous utilisez DJS, il faut compiler le fichier main.djs en main.js, c'est ce dernier qui est inclut dans index.html. Si vous avez des soucis, n'hésitez pas à m'en faire part via le formulaire de contact en attendant que je mette en place les commentaires.

 

C'est sur cette fenêtre noire que se termine la première partie de la création de jeu HTML5; dans la partie 2 nous afficherons de vrais choses à l'écran gràce à l'affichage des maps.
Sur ce, bon coding !