Small codes

HTML5, Javascript and AS3

Génération de cartes à cases hexagonales avec canvas HTML5 et createJS

| 2 Commentaires

Des cartes à cases hexagonales avec CreateJS

Cette nouvelle série d’articles aura pour thème les cartes à case hexagonales. Le sujet est plus vaste qu’on pourrait le croire. Il peut impliquer de l’architecture MVC, de la génération aléatoire, des algorithmes de pathfinding, l’implantation de biomes, la gestion des interfaces utilisateurs, la gestion côté serveur et les connexions par sockets. Et ce n’est surement pas une liste exhaustive.

Ces derniers temps, j’ai travaillé sur une architecture de base pour une génération aléatoire de cartes avec pathfinding :

Hexagon map generator made with createjs and html5 canvas

Ce générateur fait partie d’un projet plus large, et évoluera au fil du temps. Dans les prochains posts, je présenterai certaines des techniques utilisées. Et je vais commencer dès maintenant avec l’architecture MVC utilisée avec createJS et un petit mot sur les promises (ou futures, ou deffered).

Faire du MVC avec createJS

Au fil du temps que je passe à programmer le javascript, j’éprouve de plus en plus le besoin d’organiser mon code, et de garder retrouver une logique claire, mais avec un language a prototypes non typé, le code peut vite devenir une horreur.

J’ai eu la chance de participer récemment à la Web-5 conf à Béziers. Parmis les speakers se trouvait Kamil Trebunia, qui présentait des concept d’architecture de jeux HTML5. Le générateur de carte était terminé à ce moment là, et fonctionnait, mais après avoir vu cette présentation, je l’ai modifié en profondeur pour en faire une architecture Model View Controller

Quel est l’intérêt ?

L’intérêt principal pour moi, c’est qu’avec une vue découplée du contrôle et des données, on a d’abord une grande clarté de code. Ce qui facilite grandement la maintenance, la modification, et l’ajout de nouvelles fonctionnalités. Sur un projet qui évolue sans cesse, il est essentiel de garder les choses en ordre. Cette architecture rend aussi possible de faire passer certains traitements côté serveur, par exemple, la gestion du biome, et d’autres côté client, par exemple, le rendu.

MVC architecture with EaselJS

Le point d’entrée

Le point d’entrée de l’application est le fichier main.js. Son rôle est de charger les fichiers de données, créer un namespace et y stocker des variables globales, puis mettre en route le système.

//some parts of the code were written by Kamil Trebunia.
(function () {
	'use strict';
	
	window.ylende = {};
	var screenWidth,screenHeight,wrapper,agent,preloader;
	
	ylende.SPRITE_MAP_URL = "data/sprites.json";
	ylende.SOUND_MAP_URL = "sounds.json"; //not yet used
	ylende.CONFIG_URL = "data/config.json";
	
	window.addEventListener("load", function () {
		
		if(!(!!document.createElement('canvas').getContext)){
				wrapper = document.getElementById("canvasWrapper");
				wrapper.innerHTML = "Your browser does not appear to support " + "the HTML5 Canvas element";
				return;
		}
		
		ylende.agent = window.navigator.userAgent.toLowerCase();
		
		var configD = ylende.net.getJSON(ylende.CONFIG_URL);
		var spriteMapD = ylende.net.getJSON(ylende.SPRITE_MAP_URL);
		var imagesD = new ylende.Deferred();
				
		spriteMapD.addCallback(function (spriteMap) {
			
			var objectsURIs = Object.keys(spriteMap);
			var loadedObjectsNumber = 0;
			
			objectsURIs.forEach(function (uri) {
				
				spriteMap[uri].image = new Image();
				spriteMap[uri].image.addEventListener("load", function () {
					loadedObjectsNumber += 1;
					if (loadedObjectsNumber === objectsURIs.length) {
						imagesD.callback();
					}
				}, false);
				spriteMap[uri].image.addEventListener("error", function () {
					imagesD.errback();
				}, false);
				spriteMap[uri].image.setAttribute("src", spriteMap[uri].url);
			});
		});
		
		ylende.Deferred.gatherResults([spriteMapD, configD, imagesD]).addCallback(function () {
			ylende.images = spriteMapD.result[0];
			
			ylende.mapDataStore = new ylende.Store();
			var core = new ylende.Core({
				entityTypes: spriteMapD.result[0],
				config: configD.result[0]
			});
			core.init();
			
			ylende.view = new ylende.View(core);
			ylende.view.play();
			
		});
		
		
	}, false);
	
})();

Le chargement des données – pour l’instant, ce sont des fichiers de configuration JSON – est assuré par l’intermédiaires de Deferreds. Cette technique est pratique pour le chargement non bloquant de données, puisque les callbacks sont asynchrones. Elle permet aussi de rassembler les résultats dans une seule et même condition, comme ici : le code ne s’exécute que lorsque les Deferreds sont tous passés en state State.SUCCESS.

Le contrôleur principal : la classe Core

Ensuite, nous commençons la séparation du code en créant une instance de Core. La classe Core est le contrôleur général de l’application. Je lui passe mes objets de données en paramètre, ce qui lui permet de créer ses propres données : la carte. L’objet Store n’est pas utilisé pour l’instant. J’en parlerai un peu plus tard.

ylende.Deferred.gatherResults([spriteMapD, configD, imagesD]).addCallback(function () {
			ylende.images = spriteMapD.result[0];
			
			ylende.mapDataStore = new ylende.Store();
			var core = new ylende.Core({
				entityTypes: spriteMapD.result[0],
				config: configD.result[0]
			});
			core.init();
[...]

Voici le code de la classe Core

(function () {

	'use strict';
	
	function Core(config) {
		this.initialize(config);
	}
	
	Core.prototype.initialize = function(config){
		this.map = {};
		this.config = config;
	}
	
	Core.prototype.init = function(){
		this.entityTypes = this.config.entityTypes;
		this.hexMap = new ylende.HexMap();
		
		if (!this.config.map){
			this.map = this.hexMap.generateMap(this.config.config.biome);
		}else{
			this.map = this.hexMap.setMap(this.config.map);
		}
	}
	window.ylende.Core = Core;
})();

La classe ne fait qu’inspecter si des données de carte sont présentes dans l’objet de donnée. Si ce n’est pas le cas, il crée un nouvel objet controleur de carte (instance de HexMap). La classe HexMap contient les scripts de génération des données de la carte (je vous en parle dans le prochain post).

La vue principale : la classe View

Retour dans le script de lancement main.js, où j’instancie la classe View :

[...]
        ylende.view = new ylende.View(core);
        ylende.view.play();
       	});

… en lui passant le contrôleur général Core en paramètre. La View est la vue générale de l’application. Elle va servir à recueillir les évènements généraux du window (onresize, onscroll…), mais aussi du canvas, et donc du stage de createJS. Ainsi, la View contient le stage, tandis que tous les autres objets de type view (comme les hexagones) seront des éléments de EaselJS dans le stage (containers, movieclips…).

Voici le code la classe view.

(function () {

	'use strict';
	
	function View(controller) {
		this.initialize(controller);
	}
	var canvas = document.getElementById("stageCanvas");
	var map;
	
	View.prototype.initialize = function(controller){
		
		this.gameBounds = {top: 0, bottom: canvas.height,right: canvas.width, left: 0};
		this.controller = controller;
		this.stage = new Stage(canvas);
		this.stage.enableMouseOver();
		
		Touch.enable(this.stage);
		Ticker.setFPS(32);
		Ticker.addListener(this.stage);
		
		map = new ylende.HexMapView(this);
		this.stage.addChild(map);
		
		this.inviteText = new Text("Click on it, drag it - MouseWheel to zoom in and out - Double click to center", "bold 11pt courier","#FFF");
		this.inviteText.textAlign = "center";
		this.inviteText.x = this.gameBounds.right * .5;
		this.inviteText.y = this.gameBounds.bottom - 15;
		this.stage.addChild(this.inviteText);
		
		window.onorientationchange = function (e) {
			//console.log(e);
		};
		
		window.onresize = function (e) {
			//console.log(e);
		};
		
		window.onscroll = function (e) {
			//console.log(e);
			return false;
		};
		
		window.onmousewheel = function(e){
			//console.log(e);
			map.resize(e.wheelDelta);
		}
		//firefox users...
		window.addEventListener('DOMMouseScroll', scroll, false);
		function scroll(e){
			map.resize(e.detail*-0.01);
		}
	}
	
	View.prototype.play = function(){
		Ticker.setFPS(32);
		Ticker.addListener(this);
	}
	
	View.prototype.tick=function(){
	}
		
	window.ylende.View = View;
})();

La classe View crée un objet de type HexMapView, qui va recevoir les données de cartes créées par la classe HexMap, et va se charger de les afficher. Ce schéma est reproduit pour chaque objet de la carte (hexagones…), avec un contrôleur qui gère ses données et une vue qui les affiche.

A noter que dans un MVC classique, les classes de vues implementent toutes une methode draw(). Celle-ci n’a pas lieu d’être avec CreateJS, puisque l’objet Ticker met à jour les vues de manière automatique.

Et les données ? La classe Store

Du côté du Modèle, pour l’instant, j’ai juste créé une classe Store() minimale, que je compte développer plus tard, afin de rendre les données évolutives dans le temps.

Conclusion : CreateJS et le MVC

Au final, CreateJS se prête bien au MVC. Il peut sembler fastidieux d’organiser son code de cette manière, mais en réservant à EaselJS la seule mission de l’affichage, le code est bien plus propre et logique, et au final on travaille mieux et plus vite.

Dans le prochain post, je montrerai comment je génère la carte, et parlerai de l’algorithme de Dijkstra et son utilisation dans un contexte hexagonal.

2 Commentaires

  1. This could be useful for making a game. Too bad it is rather slow (about 8fps)

Laisser un commentaire

Champs Requis *.


Social Widgets powered by AB-WebLog.com.