{
.
/
#
o
$
!

NOUS LES DEVS

Commande clavier

A la conquête du jeu partie 2

Niveau : intermédiaire
</> </> </>

L'interface

Ça fait quelques jours que je n’ai pas rédigé d’article sur le jeu, car je ne savais pas trop sur quelle partie avancer précisément. J’ai bricolé deux ou trois trucs sur différentes parties du jeu, comme l’ajout d’unités sur la map et le déplacement de celles-ci, et c’est cool car ça marche plutôt bien. J’ai même montré ça à un collègue de bureau et on a déliré là-dessus. Le fait d’avoir commencé à chipoter à des parties un peu plus complexes m’a fait réfléchir sur une nouvelle step Avant de pouvoir déplacer des unités sur la map, il faut pouvoir les construire, c’est-à-dire pouvoir pointer une base ou un chantier naval et actionner l’achat de ceux-ci.

Donc aujourd’hui, je vais bosser sur les commandes au clavier, dans le jargon de l’informatique, on appelle ça l’interface, parce que ça claque en soirée !

Clavier ou souris ?

Telle est la question ! Après avoir réfléchi, j’ai choisi de développer un contrôle au clavier pour rester fidèle au jeu. Je sais que le contrôle souris peut apporter un certain confort, je vais voir si je peux l’intégrer au passage.

Grid cursor

Une chose à laquelle je n’avais pas pensé, c’est l’ajout du curseur sur la map. Le curseur déborde énormément sur le jeu original et pour rester fidèle à celui-ci, je ne vais pas avoir le choix que de créer une nouvelle grille/map par dessus les autres. Je pourrais aussi créer une balise enfant dans la grille des unités au niveau de la classe bg, si tu te souviens de ce que j’avais fait dans la partie 1 au sein de la section débordement. Mais je me dis que créer une nouvelle grille n’est pas problématique et ça pourrait peut être même me servir pour après.

Donc au niveau du code c’est pas sorcier, je lance une nouvelle fonction qui crée une map avec l’id map_cursor. J’ai également optimisé ma fonction avec la nouvelle syntaxe ES6, et je duplique mon code SASS, en veillant à ce que cette nouvelle grille soit placée au-dessus des autres.

construct_cursor_grid = (map) => {
	//ajout de la grid des unités
	$("#grid").append('<div id="map_cursor"></div>');

	map.forEach( (line, index_line) => {
		$("#map_cursor").append('<div class="tr">');
		line.forEach( (col, index_col) => {
			$(`#map_cursor .tr:eq(${index_line})`).append(`<div class="td" data-x="${index_col}" data-y="${index_line}"><div class="bg"></div></div>`);	
		});
		$("#map_cursor").append('</div>');		
	});
}
#grid{
	position: relative;
	margin: 10px;
	#map{
		position: absolute;
		left: 0;
		top:0;
		z-index: 0;
	}
	#map_units{
		position: absolute;
		left: 0;
		top:0;
		z-index: 1;
	}
	#map_cursor{
		position: absolute;
		left: 0;
		top:0;
		z-index: 2;	
	}
}

The cursor

Malheureusement, je n’ai pas un curseur dans mon sprite, j’ai donc regardé à quoi ressemble le curseur d’AW (Advance Wars) sur mon émulateur. Le curseur est assez simple, il a 4 coins qui sont représentés par des triangles et un doigt (pour le coin inférieur droit). De plus, le curseur est animé avec un effet de « va et vient » vers le centre.

Et comme j’ai dit que je ferai 100% du job, je suis parti pour créer le curseur à la main. Je pense que la solution la plus simple est de faire un gif animé, donc c’est parti pour une petite session de photoshop. Je ne te détaille pas la création du gif, je crée juste 4 pièces et je fais une animation d’image. Pour chaque step, je décale chaque pièce vers l’extérieur de 2px et je m’arrange pour que le mouvement soit fluide.

curseur_creation_gif

Pour afficher le curseur, j’ai simplement placé une classe cursor sur un élément de la map cursor et je trouve que l’exemple est relativement satisfaisant, en tout cas je suis content de moi !

exemple_curseur_map

P.S. Tu as vu mon super tank sur l’image du dessus, il en jette hein !

Commande clavier

Allez, un peu de code maintenant ! Je crée une fonction set_cursor() qui va se charger de placer le curseur à un endroit par défaut et qui va ajouter un événement affichant le code de la touche lorsque j’appuie sur une touche du clavier, le but étant de récupérer les 4 valeurs des touches directionnelles (les flèches sur le clavier quoi).

set_cursor = (x = 0, y = 0) => {
	//ajout du curseur
	$("#map_cursor .td[data-x="+x+"][data-y="+y+"]").addClass("cursor");

	document.onkeydown = (e) => {
	
		e = e || window.event;
		console.log(e);
	};
}

set_cursor(0, 0);

En console, je peux voir keydown charCode=0, keyCode=39, la valeur des touches directionnelles est donc stockée dans la méthode « KeyCode » de l’objet. Au niveau de mon code, je stocke les différentes valeurs dans un tableau et au passage je stocke aussi les valeurs de la touche y et x qui feront office de touches a et b mais j’y reviendrai plus tard.

Pour déplacer mon curseur, je détecte si la touche est bien une touche de déplacement, via un indexOf() et je vérifie aussi que la nouvelle position est bien une position valide dans la map, par exemple si le curseur est en x=0 et y=0, je ne peux pas monter ni aller à gauche, donc je passe en paramètres la taille de ma map (width et height) pour gérer les autres cas.

Pour plus de simplicité et éviter la redondance, je crée une fonction move_cursor() qui va ajouter la classe cursor uniquement à la nouvelle position (un p’tit clean juste avant pour enlever la classe cursor de tout autre élément).

//fonction qui ajoute la classe cursor à la nouvelle position
move_cursor = (x, y) => {
	//remove cursor to old position
	$("#map_cursor .td").removeClass("cursor");
	//add cursor to new position
	$("#map_cursor .td[data-x="+x+"][data-y="+y+"]").addClass("cursor");
}

//fonction qui ajoute le curseur
set_cursor = (x = 0, y = 0, width, height) => {
	//ajout du curseur
	$("#map_cursor .td[data-x="+x+"][data-y="+y+"]").addClass("cursor");
	
	//gauche = 37
	//droite = 39
	//bas =  40
	//haut = 38
	let direction = [37, 38, 39, 40];
	//y = 121
	//x = 120
	let command = [121,120];
	
	document.onkeydown = (e) => {
	
		e = e || window.event;

		//si c'est une touche de direction
		if(direction.indexOf(e.keyCode) != -1){

			//gauche
			if(e.keyCode == 37){
				if( x > 0 ){
					move_cursor(x-1, y);
				}
			}
			//droite
			if(e.keyCode == 39){
				if( x < width-1 ){
					move_cursor(x+1, y);
				}
			}
			//haut
			if(e.keyCode == 38){
				if( y > 0 ){
					move_cursor(x, y-1);
				}
			}
			//bas
			if(e.keyCode == 40){
				if( y < height-1 ){
					move_cursor(x, y+1);
				}
			}		
		}
	}
}

Et là, je me rends compte que ça ne marche seulement que pour une seule case dans chaque direction, car j’ai oublié de récupérer la nouvelle position du curseur à chaque nouveau déplacement.

//si c'est une touche de direction
if(direction.indexOf(e.keyCode) != -1){
	//recupere la position de x et y
	let x = parseInt( $("#map_cursor .td.cursor").attr("data-x") );
	let y = parseInt( $("#map_cursor .td.cursor").attr("data-y") );

	//...
}

Je viens de faire quelques tests et je suis plutôt satisfait, le code m’a pris une quinzaine de minutes à faire et c’est déjà fonctionnel. Bon par contre… Un truc vraiment pas cool, c’est que lorsque j’appuie sur deux touches (bas et droite par exemple) les deux mouvements sont effectués mais si je maintiens les deux touches appuyées, l’événement onkeydown va seulement s’effectuer sur la dernière touche. C’est logique mais bon voilà, moi je veux un déplacement en diagonale quand j’appuie sur deux touches en même temps.

Après plusieurs tests et chamboulements dans mon code, j’ai trouvé un truc pas mal. Je vais stocker les touches appuyées dans un tableau et je fais un foreach sur chaque élément de ce tableau et je relance le déplacement du curseur. J’ajoute aussi un nouvel événement onkeyup, pour gérer le fait qu’une touche est relâchée. A ce moment précis, je la retire du tableau, comme ça, le mouvement peut continuer seulement avec la ou les touches présentent dans le tableau.

type_move = (code, width, height) => {
	//recupere la position de x et y
	let x = parseInt( $("#map_cursor .td.cursor").attr("data-x") );
	let y = parseInt( $("#map_cursor .td.cursor").attr("data-y") );
	
	//gauche
	if(code == 37){
		if( x > 0 ){
			move_cursor(x-1, y);
		}
	}
	//droite
	if(code == 39){
		if( x < width-1 ){
			move_cursor(x+1, y);
		}
	}
	//haut
	if(code == 38){
		if( y > 0 ){
			move_cursor(x, y-1);
		}
	}
	//bas
	if(code == 40){
		if( y < height-1 ){
			move_cursor(x, y+1);
		}
	}	
}


//fonction qui ajoute le curseur
set_cursor = (x = 0, y = 0, width, height) => {
	//ajout du curseur
	$("#map_cursor .td[data-x="+x+"][data-y="+y+"]").addClass("cursor");
	
	//gauche = 37
	//droite = 39
	//bas =  40
	//haut = 38
	let direction = [37, 38, 39, 40];
	//y = 121
	//x = 120
	let command = [121,120];
	//variable qui sert à stocker les touche multiple
	let key_press = [];
	
	document.onkeyup = function (e) {
		
		e = e || window.event;
		let position = key_press.indexOf(e.keyCode);
		if( position != -1 ){
			//suppression des touche relacher
			key_press.splice(position, 1);
		}
	}
	document.onkeydown = (e) => {
	
		e = e || window.event;

		//si c'est une touche de direction
		if(direction.indexOf(e.keyCode) != -1){
			//stocke la touche entrer si elle n'est pas dedans
			if( key_press.indexOf(e.keyCode) == -1 ){
				key_press.push(e.keyCode);
			}
			
			//pour chaque touche enfoncé executer le code suivant
			key_press.forEach((code, index) => {
				type_move(code, width, height);
			});
		}
	}
}

Lorsque j’appuie sur bas et droite et que je relâche la deuxième touche, le mouvement ne continue pas vers le bas. A l’inverse, si je relâche ma première touche, le mouvement continue à droite. En fait, ça vient du fait que lorsque que j’appuie sur la deuxième touche, c’est cette pression sur cette touche qui génère l’événement onkeydown (c’est la dernière touche qui prend la main, si je peux dire ça comme ça) et là je ne sais pas comment je peux faire pour régler ce problème, car je ne peux pas simuler la pression sur la première touche à cause des mécanismes de sécurité des navigateurs. Si tu as une solution, je suis preneur !

Quand j’appuie très rapidement sur deux touches, par exemple droite et bas, l’événement onkeydown est exécuté deux fois, lors de la première pression le déplacement va se faire d’abord à droite (un seul élément dans le tableau des touches) et lors de la seconde pression les deux déplacements vont se refaire (deux éléments dans le tableau), donc le déplacement final pour cette combinaison est de deux cases à droite et une case vers le bas, et ensuite le déplacement continue de manière diagonale. Le soucis c’est ce décalage de deux cases, pour éviter cela j’ai dû créer une nouvelle variable touche_simultane que je définis à false si il y a deux touches pressées, je ne fais rien si ce n’est mettre touche_simultane à true, dans ce cas il n’y a pas de déplacement, et juste après, l’événement onkeypress va re-exécuter le code et donc déplacer mon curseur en diagonale proprement.

Pour éviter les bugs, j’ai lancé le déplacement d’une seule touche sur l’événement onkeyup et je réinitialise touche_simultane à false à chaque appel de cet événement.

//fonction qui ajoute le curseur
set_cursor = (x = 0, y = 0, width, height) => {
	//ajout du curseur
	$("#map_cursor .td[data-x="+x+"][data-y="+y+"]").addClass("cursor");

	//gauche = 37
	//droite = 39
	//bas =  40
	//haut = 38
	let direction = [37, 38, 39, 40];
	//y = 121
	//x = 120
	let command = [121,120];
	//variable qui sert à stocker les touches multiples
	let key_press = [];
	//si deux touches sont pressées simultanément alors true
	let touche_simultane = false;
	
	document.onkeyup = function (e) {
		
		//on repasse touche_simultane à false
		touche_simultane = false;
		
		e = e || window.event;
		let position = key_press.indexOf(e.keyCode);
		if( position != -1 ){
			//suppression des touches relachées
			key_press.splice(position, 1);
		}
	}
	document.onkeydown = (e) => {
	
		e = e || window.event;
		
		//si c'est une touche de direction
		if(direction.indexOf(e.keyCode) != -1){

			//stocke la touche entrer si elle n'est pas dedans
			if( key_press.indexOf(e.keyCode) == -1 ){
				key_press.push(e.keyCode);
			}
			
			//si c'est une seule touche le mouvement est executé au relachement
			if(key_press.length > 1){

				//pour chaque touche enfoncée executer le code suivant
				key_press.forEach((code, index) => {
					//si simultané est à false, on n'execute pas la première touche trouver car elle a déjà été executée
					//ensuite on passe simultané à true pour executer la combinaison de touches
					if(touche_simultane == false && index == 0){
						touche_simultane = true;
					}else{
						type_move(code, width, height);
					}
				});
			}else{

				type_move(e.keyCode, width, height);
			}		
		}
	};
}

Souris

En comparant la vitesse de déplacement entre ce que j’ai fait et l’émulateur, c’est un peu plus rapide sur l’émulateur et là je me dis qu’ajouter le déplacement à la souris ce serait p’tet pas mal.

Et en fait, c’est tout con, il suffit d’utiliser l’événement hover pour changer le curseur de place…

//fonction qui ajoute le curseur + événement souris
set_cursor = (x = 0, y = 0, width, height) => {

	//...

	$("#map_cursor .td").hover(function(e){
		move_cursor($(this).attr("data-x") , $(this).attr("data-y") );
	});
}

Mais il y a quelque chose qui ne va pas ! En fait, si j’agrandis volontairement la zone du curseur, représentée ci-dessous par le bord rouge de 3px, une partie de la map ne sera pas capable de changer la position du curseur. le hover ne peut pas se faire car l’élément intérieur du curseur passe par dessus les autres éléments td de la grille. Et en plus, d’après les quelques tests que je viens de faire, je peux seulement aller sur les cases rouges foncé, c’est vraiment bizarre.

map_cuseur_probleme_hover

Pour régler le problème, rien de plus simple, je définis le z-index des éléments .td de #map_cusor à 2 et si jamais un de ces td a la classe cursor alors je définis sont z-index à 1, et le tour est joué.

Voici le résultat final (début au clavier et ensuite la souris).

17/04/2016

Yann Vangampelaere - nouslesdevs -

NOUS LES DEVS

Vous aimez ce que je fais ? Vous voulez que j'en fasse plus ? dans le développement du blog.