Connectez votre rameur d’appartement avec Chrome - Partie 2

Gestion des interactions

Afin de pouvoir démarrer l’application, nous devons gérer les clicks sur le canvas. Le problème est que lorsque nous dessinons des images, nous ne pouvons pas avoir accès à un équivalent de onClick sur une zone graphique précise. Nous devons donc écouter les clicks sur le canvas et calculer si la zone de click correspond à une zone d’interaction de notre ihm.

// Gère les clicks en fonction de l’état du jeux

function checkClick(event) {

if (gameModel.stateGame != constState.STATE_RUNNING) {

var btnStart = ui.resources.images[‘btn_start’];

var finalHeight = btnStart.height * ui.ratio,

finalWidth = btnStart.width * ui.ratio;

var x = (ui.canvas.width / 2) – ((btnStart.width * ui.ratio) / 2), y = ui.canvas.height – finalHeight – (isPortrait() ? 100 : 50);

var xClick = event.pageX,

yClick = event.pageY;

if (yClick > y && yClick < (y + finalHeight) && xClick > x && xClick < (x + finalWidth)) {

// On change l’état du jeux

gameModel.stateGame = gameModel.stateGame === constState.STATE_ACCUEIL ? constState.STATE_RUNNING : constState.STATE_ACCUEIL;

}

}

}

Gestion du moteur

Un des éléments clés du programme est l’alimentation du modèle depuis l’application chrome. L’application chrome va alimenter le modèle et déterminer un certain nombre d’éléments en lien avec le rendu souhaité, puis appeler le moteur de calcul pour terminer les traitements. On vérifie par exemple dans quel sens va le joueur, s’il y a eu un déplacement, etc.

// Calcul

function setDistance(distance) {

if (gameModel.distanceArduino === distance) {

gameModel.direction = 0;

}

else if (gameModel.distanceArduino > distance) {

gameModel.direction = 1;

}

else {

gameModel.direction = – 1;

}

// Vitesse en cm / ms

var deltaCM = Math.abs(gameModel.distanceArduino – distance);

if (deltaCM > ConstSAS.MIN_DELTA_CM) {

gameModel.speed = deltaCM / ConstSAS.DELAY;

}

else {

gameModel.speed = Math.max(gameModel.speed – ConstSAS.FACTOR_SPEED,

0);

}

gameModel.distanceArduino = distance;

engineSkiff();

}

Le coeur du moteur quant à lui, s’occupe uniquement de lire les données issues de l’arduino et de calculer la distance globale parcourue, ainsi que le pourcentage de déplacements dans l’écran.

function engineSkiff() {

if (gameModel.speed > 0 && gameModel.stateGame === constState.STATE_RUNNING) {

var distanceSpeed = gameModel.speed * ConstSAS.DELAY;

// On incrémente la distance

gameModel.distanceSkiff += (distanceSpeed * ConstSAS.FACTOR_DISTANCE);

// On gère l’effet de déplacement des bords via un pourcentage

gameModel.percent = (gameModel.distanceSkiff % 100) / 100;

}

}

Spécifique Chrome

Depuis le début nous parlons d’application chrome et de l’API Serial, il est désormais temps de voir comment nous implémentons cette partie.

Interaction avec l’Arduino

Toute l’interaction avec L’arduino se fait directement via le port série. Mais il faut pour cela passer par des étapes clés :

  1. récupérer les appareils connectés sur les ports série “chrome.serial.getDevices”
  2. une fois un appareil trouvé, on s’y connecte “chrome.serial.connect”. Dans notre exemple, on veille à ce qu’il n’y ai qu’un appareil de connecté au moment du lancement de l’application
  3. lire le port série “chrome.serial.onReceive”

 

function initArduino() {

chrome.serial.getDevices(function(ports) {

if (ports && ports.length == 1) {

chrome.serial.connect(ports[0].path,

onOpenArduino);

}

}

);

}

function onOpenArduino(openInfo) {

connectionId = openInfo.connectionId;

console.log(« connectionId:  » + connectionId);

if (connectionId == – 1) {

console.log(‘Could not open’);

return;

}

console.log(‘Connected’);

chrome.serial.onReceive.addListener(onReadArduino);

}

function convertArrayBufferToString(buf) {

return String.fromCharCode.apply(null,

new Uint8Array(buf));

}

function onReadArduino(readInfo) {

if (readInfo.connectionId == connectionId && readInfo.data) {

var str = convertArrayBufferToString(readInfo.data);

if (str.charAt(str.length – 1) === ‘\n’) {

value += str.substring(0,

str.length – 1);

if (regExp.test(value)) // Light on and off

{

var distanceTmp = + regExp.exec(value)[1];

if (distanceTmp < ConstSAS.DISTANCE_MAX && Math.abs(distance – distanceTmp) < (ConstSAS.DISTANCE_MAX * 1.5) ) {

AppSAS.setDistance(distanceTmp);

}

distance = distanceTmp;

}

value = «  »;

}

else {

value += str;

}

}

}

Il faut noter une petite particularité lors de la lecture : nous lisons le port série et nous attendons une chaîne de caractères. Aussi, il est important de penser à convertir les données issues du port série en données exploitables sous forme de chaîne de caractères.

Gestion des écrans et cas particuliers

Maintenant que nous avons vu la mécanique sous le capot, regardons de plus près quelques cas particuliers qui méritent un peu d’attention.

Déplacement du décor

Afin de donner une sensation de déplacement, il nous faut bouger nos rives. Pour se faire, on va simplement fonctionner avec un indicateur en pourcentage en rapport avec le déplacement global du rameur. Ainsi pour afficher correctement nos 2 rives qui bougent, il nous suffit juste de dessiner 2 fois chaque rive de chaque côté afin de gérer les dépassement d’écran et de les positionner en fonction d’un pourcentage résultant d’un modulo de la distance du rameur :

// Affiche le rivage en fonction de la rive souhaitée et de la progression du rameur

function paintRive(riveDroite) {

var rive = ui.resources.images[(riveDroite ? ‘rive_droite’ : ‘rive_gauche’) + getSuffix()];

var finalHeight = rive.height * ui.ratio,

finalWidth = rive.width * ui.ratio;

ui.context.drawImage(rive , 0 //sx clipping de l’image originale

, 0 //sy clipping de l’image originale

, rive.width // swidth clipping de l’image originale

, rive.height // sheight clipping de l’image originale

, riveDroite ? ui.canvas.width – finalWidth : 0 // x Coordonnées dans le dessing du canvas

, 0 – (finalHeight * gameModel.percent) // y Coordonnées dans le dessing du canvas

, finalWidth // width taille du dessin

, finalHeight // height taille du dessin

);

ui.context.drawImage(rive , 0 //sx clipping de l’image originale

, 0 //sy clipping de l’image originale

, rive.width // swidth clipping de l’image originale

, rive.height // sheight clipping de l’image originale

, riveDroite ? ui.canvas.width – finalWidth : 0 // x Coordonnées dans le dessing du ui.canvas

, finalHeight – (finalHeight * gameModel.percent) // y Coordonnées dans le dessing du canvas

, finalWidth // width taille du dessin

, finalHeight // height taille du dessin

);

}

L’affichage du rameur

L’affichage du rameur comporte 2 parties à prendre en compte :

  1. l’affichage du rameur en fonction de la position du joueur
  2. l’avancée du rameur quand le mode ghost est activé

Position du rameur en fonction du joueur

Afin de restituer au mieux les gestes effectués par le joueur, il a fallu réfléchir à une façon d’afficher la bonne image de rameur, en fonction de sa position sur le rameur.

La réponse était relativement simple :  il suffit de connaître à chaque instant la position du joueur sur le rameur et sa direction, puis d’appliquer la bonne image.

function indexToUse(direction, distance) {

var arrayToUse = direction >= 0 ? mappingPositonRameurFront : mappingPositonRameurBack;

for (var i = 0; i < arrayToUse.length; i++) {

var minMax = arrayToUse[i];

if (distance > minMax.min && distance <= minMax.max) {

return minMax.indexSprite + AppSAS.getSuffix();

}

}

return mappingPositonRameurBack[0].indexSprite + AppSAS.getSuffix();

}

// Affiche le bon sprire du bateau

function paintBoat() {

//var ratio = 0.05;

var image = AppSAS.ui.resources.images[AppSAS.gameModel.indexSprite];

AppSAS.ui.context.shadowOffsetX = 0;

AppSAS.ui.context.shadowOffsetY = 0;

AppSAS.ui.context.shadowBlur = 0;

AppSAS.ui.context.drawImage(image , 0 //sx clipping de l’image originale

, 0 //sy clipping de l’image originale

, image.width // swidth clipping de l’image originale

, image.height // sheight clipping de l’image originale

, (AppSAS.ui.canvas.width / 2) – ((image.width * AppSAS.ui.ratio) / 2) // x Coordonnées dans le dessin du AppSAS.ui.canvas

, (AppSAS.ui.canvas.height / 2) – ((image.height * AppSAS.ui.ratio) / 2) + 100// y Coordonnées dans le dessin du AppSAS.ui.canvas

, image.width * AppSAS.ui.ratio // width taille du dessin

, image.height * AppSAS.ui.ratio // height taille du dessin

) }

De cette manière nous affichons toujours la bonne image. Puis, afin de faciliter le déplacement du bateau, il a été considéré qu’il était en position fixe sur l’écran et que l’illusion du déplacement se fait uniquement à travers le déplacement du décor.

Gestion du ghost

L’affichage du ghost doit faire face à un problème. Contrairement au bateau, ce dernier se déplace sur l’écran. Le problème est que ce déplacement vient surtout du fait qu’il a fallu gérer le cas d’un ghost allant plus vite que le joueur courant. En effet, si le ghost est meilleur que le joueur, alors, il sera vers le haut de l’écran ce qui veut dire que sa position va s’approcher de l’axe, voire aller dans les négatifs. Or dans un canvas, si un élément est dessiné avec des coordonnées de destination dans le négatif, l’élément n’est tout simplement pas peint !  Il a donc fallu tronquer l’image source pour donner l’illusion que le dessin du ghost parte vers le haut de l’écran.

 

// Affiche le bon sprire du bateau du mode Ghost

function paintGhost() {

//var ratio = 0.05;

var image = AppSAS.ui.resources.images[AppSAS.gameModel.indexSpriteGhost];

// Le fantome doit etre dessiné là où il est au niveau de sa distance globale par rapport au bateau actuel

// => On l’affiche là où est son delta en distance par rapport à bateau actuel

var stateGhost = AppSAS.gameModel.ghost[AppSAS.gameModel.step];

var deltaGhost = AppSAS.gameModel.distanceSkiff – stateGhost.distanceSkiff;

// On doit tronquer le ghost s’il dépasse de l’écran

var yGhost = (AppSAS.ui.canvas.height / 2) – ((image.height * AppSAS.ui.ratio) / 2) + 100 + (deltaGhost * ConstSAS.FACTOR_GHOST);

var heightSpriteGhost = image.height;

if (yGhost < 0) {

heightSpriteGhost = image.height + yGhost;

yGhost = 0;

}

AppSAS.ui.context.drawImage(image , 0 //sx clipping de l’image originale

, image.height – heightSpriteGhost //sy clipping de l’image originale

, image.width // swidth clipping de l’image originale

, heightSpriteGhost // sheight clipping de l’image originale

, (AppSAS.ui.canvas.width / 2) – ((image.width * AppSAS.ui.ratio) / 2) // x Coordonnées dans le dessin du AppSAS.ui.canvas

, yGhost // y Coordonnées dans le dessin du AppSAS.ui.canvas , image.width * AppSAS.ui.ratio // width taille du dessin

, heightSpriteGhost * AppSAS.ui.ratio // height taille du dessin

);

}

Affichage des écrans de login & de scores

Ces 2 écrans sont différents car on y affiche non pas une animation, mais tu texte.

Ecran de Login

rameur connecté

Concernant l’écran de login, il n’existe pas d’équivalent du champ input dans un canvas. Aussi, il a fallu intégrer à notre html une balise input que l’on affiche ou cache en fonction du besoin.

Ecran de scores

écran des scores

L’affichage du score est simple car il ne s’agit que d’afficher du texte :

AppSAS.ui.context.fillText(MyText’, x, y);

La taille et la couleur du texte sont définis par des propriétés appliquées directement sur le context du canvas.

Persistance des données

Pour sauvegarder les données d’une partie à l’autre, ou même d’un démarrage d’application à l’autre. J’ai fait le choix le plus simple : le LocalStorage. Le seul hic avec le LocalStorage et les ChromeApps, c’est que l’API telle qu’elle est disponible en html5 n’existe pas sur une ChromeApp. En effet, l’api localStorage étant syncrhone, Google a préféré mettre en place une solution asyncrhone : https://developer.chrome.com/apps/storage. J’ai donc mis en place la solution de Google et j’en ai profité pour prévoir une api uniforme entre localstorage & chrome.storage au cas où vous partiriez sur une solution native html5.

Placer l’arduino

Afin de mesurer au mieux les données de distance, j’ai placé l’arduino au dos de l’utilisateur et j’ai fabriqué une petite boîte pour packager un peu tout ça :

placement arduino

Résultat :

du fun!

rameur rameur-1

Annexes

Le code complet est disponible https://github.com/sqli-nantes/skiff-simulator

L’article est également disponible sur le blog de l’auteur : http://jef.binomed.fr

 

Jean François Garreau

Consultant Expert Innovation SQLI Nantes

0 commentaires

votre commentaire

Se joindre à la discussion ?
Vous êtes libre de contribuer !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Inscription newsletter

Ne manquez plus nos derniers articles !