Coder le jeu video html5 pong – orientation écran

 

Vous avez testé le jeu vidéo pong html5 sur un mobile ou une tablette et là horreur!!! Le jeu est tout moche lorsque vous l’exécuter en mode portrait. Normal, puisque le jeu vidéo pong html5 est conçu pour être joué en mode paysage. Il va donc falloir arranger cela en obligeant le joueur à disposer son smartphone ou sa tablette en paysage. Tel est l’objet de ce dix-septième article. Tout ça avec html5 javascript.

 

Prérequis

Avoir lu les tutoriaux suivants :
– l’initialisation du projet Coder le jeu vidéo Pong;
– la mise en place de l’environnement du jeu constitué du terrain, du filet, des raquettes, de la balle et du score Coder le jeu vidéo Pong – Raquettes et Balle;
– l’animation de la balle Coder le jeu vidéo Pong – Animer la balle;
– le contrôle de la raquette par le joueur à l’aide du clavier Coder le jeu vidéo Pong – Animer les raquettes;
– le contrôle de la raquette par le joueur à l’aide de la souris Coder le jeu vidéo Pong – Controle à la souris;
– le renvoi de la balle par les raquettes Coder le jeu video html5 pong – Renvoi de la balle;
– l’ajout du son lorsque la balle cogne un mur ou une raquette Coder le jeu vidéo Pong – Ajout du son.
– l’intelligence artificielle Coder le jeu video html5 pong – Intelligence Artificielle;
– la Coder le jeu video html5 pong – Gestion du score et engagement;
– l’ajout d’un début et d’une fin à une partie Coder le jeu video html5 pong – début et fin de partie;
– la vitesse et la trajectoire de la balle Coder le jeu video html5 pong – vitesse et trajectoire de la balle;
– le look plus moderne Coder le jeu video html5 pong – Changement de look;
– l’adaptation du jeu vidéo à la taille de l’écran sur lequel il est exécuté Coder le jeu video html5 pong – adaptation de la taille de l’écran;
– le décor du jeu vidéo Coder le jeu video html5 pong – le décor;
– la correction de quelques imperfections quelques réajustements;
– la gestion du contrôle tactile Coder le jeu video html5 pong – contrôle tactile.

 

Principe

Pour forcer le joueur à disposer sa tablette ou son smartphone en mode paysage, vous allez afficher un message lui indiquant de le faire si il est en mode portrait.

Pour cela, il est nécessaire de détecter la disposition du smartphone. Et selon ce que cette détection renvoie, afficher un message incitant le joueur à changer la disposition si il est en mode portrait et dans le cas contraire (mode paysage) rien du tout.

Cette détection se fait au démarrage du jeu (pas d’une partie) ou lors de tout changement de disposition de l’écran par le joueur.

 

Détecter la disposition de l’écran de jeu

Je ne vous apprends rien en vous disant que:
– un mode portrait se définit par une hauteur d’écran plus grande que sa largeur;
– et qu’à l’inverse un mode paysage se définit par une largeur d’écran plus grande que sa hauteur.

Il suffit donc de partir de ces 2 principes simples pour définir une fonction qui renverra dans quelle position est l’écran.

Fonction générique dédiée à l’affichage: je vous propose de l’encapsuler dans le namespace javascript game.display.

game.display = {
  container : "",
  .... 
  getScreenPosition : function() {
    if ( this.width >= this.height ) {
      return "LANDSCAPE";
    } else {
      return "PORTRAIT";
    }
  },
  ....
}

Notez que les variables width et height sont précédées du mot clé this.

Il semble naturel que le namespace javascript game.display ait en information la taille de l’écran qu’il gère.

D’où l’ajout de ces 2 propriétés qui se substituent (inutile de garder 2 propriétés à l’usage identique) aux propriétés targetResX et targetResY du namespace game qui de fait disparaissent.

game.display = {
  container : "",
  width : 0,
  height : 0,
  .... 
  getScreenPosition : function() {
    if ( this.width >= this.height ) {
      return "LANDSCAPE";
    } else {
      return "PORTRAIT";
    }
  },
  ....
}

Il reste à les renseigner depuis la fonction initScreenRes du namespace javascript game.

var game = {
  .... 
  initScreenRes : function() {
    game.display.width = window.screen.availWidth;
    game.display.height = window.screen.availHeight;
    this.ratioResX = game.display.width/this.devResX;
    this.ratioResY = game.display.height/this.devResY;
  },
  ....
}

Là c’était la méthode un peu spartiate. Une technique plus moderne consiste à utiliser les media queries CSS3 par le biais de javascript et la fonction matchMedia de l’objet window, notamment la propriété orientation.

Je vous invite à consulter l’article Javascript, orientation portrait ou paysage ?.

 

L’écran d’avertissement

Le message d’avertissement et d’incitation au changement de position du smartphone s’affiche au démarrage du jeu avant toute initialisation graphique. Elle se fait lorsque l’écran bascule en mode paysage.

Ce message s’affiche aussi lorsque le joueur bascule du mode paysage au mode portrait.

Ce qui nous oblige à ne pas systématiser le redimensionnement comme cela est fait jusqu’à présent. Et donc à faire un peu de refactoring.

 

Le bloc du message

Commencez par ajouter un bloc html div centré servant à afficher le message et masqué par défaut.

<div id="screenPositionMessage" style="width:1010px;height:260px;position:absolute;left:50%;top:50%;margin:-130px 0 0 -505px;z-index:100;background:#00FF00;text-align:center;font-family:DS-DIGIB;font-size:50px;color:#FF0000;display:none;">
  Pour une experience de jeu optimale, positionnez votre smartphone ou votre tablette en <br>mode paysage.
</div>

Utilisé à de nombreuses reprises (dimensionnement, affichage..), ce nouveau bloc doit aussi être déclaré dans le namespace game.

var game = {
  blocToCenter : null,
  blocLeft : null,
  blocRight : null,
  divGame : null,
  
  screenPositionMessage : null,
  ....
}

Il convient donc aussi d’adapter ces dimensions à l’écran cible.

Ce redimensionnement est disjoint des autres pour la simple et bonne raison que le redimensionnement global se fait que lorsque l’écran a basculé en mode paysage. Le faire avant rendrait l’affichage calamiteux puisque fait sur la base de dimensions issues du mode portrait.

Et ce redimensionnement du message ne doit pas être revu lors de la bascule, le message est construit pour s’afficher uniquement en mode portrait.

Et donc une nouvelle fonction dédiée:

var game = {
  .... 
  resizeMessageElement : function() {
    this.screenPositionMessage = document.getElementById("screenPositionMessage");
    this.screenPositionMessage.style.width = game.display.width*.8 + "px";
    this.screenPositionMessage.style.height = game.display.height*.3 + "px";
    this.screenPositionMessage.style.fontSize = game.display.width*.08 + "px";
    this.screenPositionMessage.style.margin = "-" + game.display.height*.3/2 + "px 0 0 -" + game.display.width*.8/2 + "px";
  },
  ....
}

Le redimensionnement se fait sur la base des dimensions détectées. Après les facteurs utilisés ont été trouvés (je l’admets) à tâtons.

Il reste à l’appeler toujours depuis la fonction init du namespace game.

var game = {
  ....
  init : function() {

    this.initScreenRes();
    this.resizeDisplayData(conf,this.ratioResX,this.ratioResY);
	  
    this.resizeMessageElement();
    ....
  },
  ....
}

Soyez patient car à l’affichage, rien a changé.

 

Le refactoring

Refactoring relativement simple consistant à regrouper dans des fonctions dédiées et de manière logique des parties du code existant.

Pourquoi regrouper et ne pas simplement déplacer ? D’une part un couper/coller est une source potentielle d’erreur et un regroupement logique d’actions est une bonne pratique de développement.

Mais regrouper quoi ? Tout d’abord, regrouper tous les redimensionnements graphiques actuellement en vrac dans la fonction init du namespace game.

Pourquoi les regrouper ? Parce que le redimensionnement des objets graphiques doit se faire uniquement lorsque l’écran a basculé en mode paysage et donc qu’il doit être appelé uniquement à ce moment. La fonction issue de ce regroupement est appelé lorsque l’écran bascule en mode paysage.

Appelez cette nouvelle fonction resizeHtmlElement.

var game = {
  ....
  resizeHtmlElement : function() {
    this.blocToCenter.style.width = conf.BLOCCENTERWIDTH + "px";
    this.blocToCenter.style.height = conf.BLOCCENTERHEIGHT + "px";
    this.blocToCenter.style.margin = "-" + conf.BLOCCENTERHEIGHT/2 + "px 0 0 -" + conf.BLOCCENTERWIDTH/2 + "px";

    this.blocLeft.style.width = conf.BLOCLEFTWIDTH + "px";
    this.blocLeft.style.height = conf.BLOCLEFTHEIGHT + "px";

    this.blocRight.style.width = conf.BLOCRIGHTWIDTH + "px";
    this.blocRight.style.height = conf.BLOCRIGHTHEIGHT + "px";
	
    this.divGame.style.width = conf.BLOCDIVGAMEWIDTH + "px";
    this.divGame.style.height = conf.BLOCDIVGAMEHEIGHT + "px";
	
    this.startGameButton.style.width = conf.BUTTONSTARTGAMEWIDTH + "px";
    this.pauseGameButton.style.width = conf.BUTTONPAUSEGAMEWIDTH + "px";
    this.continueGameButton.style.width = conf.BUTTONCONTINUEGAMEWIDTH + "px";
	
    this.groundLayer = game.display.createLayer("terrain", conf.GROUNDLAYERWIDTH, conf.GROUNDLAYERHEIGHT, this.divGame, 0, "#000000", 10, 50); 
    game.display.drawRectangleInLayer(this.groundLayer, conf.NETWIDTH, conf.GROUNDLAYERHEIGHT, this.netColor, conf.GROUNDLAYERWIDTH/2 - conf.NETWIDTH/2, 0);
    
    this.scoreLayer = game.display.createLayer("score", conf.GROUNDLAYERWIDTH, conf.GROUNDLAYERHEIGHT, this.divGame, 1, undefined, 10, 50);
    game.display.drawTextInLayer(this.scoreLayer , "SCORE", "10px Arial", "#FF0000", 10, 10);
    
    this.playersBallLayer = game.display.createLayer("joueursetballe", conf.GROUNDLAYERWIDTH, conf.GROUNDLAYERHEIGHT, this.divGame, 2, undefined, 10, 50);  
    game.display.drawTextInLayer(this.playersBallLayer, "JOUEURSETBALLE", "10px Arial", "#FF0000", 100, 100);
	
    this.displayScore(0,0);
	
    this.ball.sprite = game.display.createSprite(conf.BALLWIDTH,conf.BALLHEIGHT,conf.BALLPOSX,conf.BALLPOSY,"./img/ball.png");
    this.displayBall();
	
    this.playerOne.sprite = game.display.createSprite(conf.PLAYERONEWIDTH,conf.PLAYERONEHEIGHT,conf.PLAYERONEPOSX,conf.PLAYERONEPOSY,"./img/playerOne.png");
    this.playerTwo.sprite = game.display.createSprite(conf.PLAYERTWOWIDTH,conf.PLAYERTWOHEIGHT,conf.PLAYERTWOPOSX,conf.PLAYERTWOPOSY,"./img/playerTwo.png");	
    this.displayPlayers();
  },
  ....
}

Et faites un appel à cette fonction en lieu et place du code dans la fonction init du namespace game.

var game = {
  ....
  init : function() {

    this.initScreenRes();
    this.resizeDisplayData(conf,this.ratioResX,this.ratioResY);
	  
    this.resizeHtmlElement();
    ....
  },
  ....
}

Mais ce n’est pas tout puisque maintenant la fonction resizeHtmlElement intègre aussi l’initialisation des éléments graphiques.

Vous allez les extraire aussi dans une fonction dédiée initHtmlElement.

var game = {
  ....
  initHtmlElement : function() {
    this.blocToCenter = document.getElementById("blocToCenter");
    this.blocLeft = document.getElementById("left");
    this.blocRight = document.getElementById("right");
    this.divGame = document.getElementById("divGame");
    this.startGameButton = document.getElementById("startGame");
    this.pauseGameButton = document.getElementById("pauseGame");
    this.continueGameButton = document.getElementById("continueGame");	
  },
  ....
}

Et appeler cette fonction avant l’appel à la fonction resizeHtmlElement dans la fonction init du namespace game.

var game = {
  ....
  init : function() {
    ....
    this.initHtmlElement();	
    this.resizeHtmlElement();
    ....
  },
  ....
}

Toujours un peu de patience, le résultat n’est pas encore là.

Vous devriez avoir la fonction init du namespace game qui ressemble à ça:

var game = {
  ....
  init : function() {

    this.initScreenRes();
    this.resizeDisplayData(conf,this.ratioResX,this.ratioResY);
	
    this.resizeMessageElement();
    this.initScreenMessage();

    this.initHtmlElement();	
    this.resizeHtmlElement();
   
    this.initKeyboard(game.control.onKeyDown, game.control.onKeyUp);
    this.initMouse(game.control.onMouseMove);
    this.initTouch(game.control.onTouchMove, game.control.onTouchStart);	
    this.initStartGameButton();
    this.initPauseGameButton();	
    this.initContinueGameButton();	
	
    this.wallSound = new Audio("./sound/wall.ogg");
    this.playerSound = new Audio("./sound/player.ogg");

    game.ai.setPlayerAndBall(this.playerTwo, this.ball);
    game.speedUpBall();
  },
  ....
} 

Dernier travail de refactoring: le regroupement de l’initialisation des événements dans une fonction initEvent du namespace game.

var game = {
  ....
  initEvent : function() {
    this.initKeyboard(game.control.onKeyDown, game.control.onKeyUp);
    this.initMouse(game.control.onMouseMove);
    this.initTouch(game.control.onTouchMove, game.control.onTouchStart);	
    this.initStartGameButton();
    this.initPauseGameButton();	
    this.initContinueGameButton();
  },
  ....
}

La fonction init après refactoring est bien plus lisible:

var game = {
  ....
  init : function() {

    this.initScreenRes();
    this.resizeDisplayData(conf,this.ratioResX,this.ratioResY);
	
    this.resizeMessageElement();
    this.initScreenMessage();

    this.initHtmlElement();	
    this.resizeHtmlElement();
   
    this.initEvent();
	
    this.wallSound = new Audio("./sound/wall.ogg");
    this.playerSound = new Audio("./sound/player.ogg");

    game.ai.setPlayerAndBall(this.playerTwo, this.ball);
    game.speedUpBall();
  },
  ....
} 

Tout est toujours pareil et rien a changé.

 

L’affichage du message

Il suffit faire apparaitre le message d’avertissement lorsque l’écran est en mode portrait au démarrage.
Au début de cet article, vous avez créé une fonction de détection de la position de l’écran, et c’est maintenant qu’il faut s’en servir.

Faites le depuis la fonction init du namespace game.

  init : function() {
    ....
    if ( game.display.getScreenPosition() == "PORTRAIT" ) {
      this.screenPositionMessage.style.display = "block";
    }
  },
  ....
}

Ce qui donne à l’affichage
Message Avertissement

D’abord il y a le problème de l’affichage derrière le message qui est chaotique.

Ensuite, si vous démarrez le jeu en mode portrait ça fonctionne au démarrage du jeu, mais il est impossible de jouer puisque la disparition du message n’est pas gérée lorsque vous passez en mode paysage et le redimensionnement ne se fait pas non plus.

Remarquez que si vous démarrez en mode paysage tout fonctionne correctement.

 

Basculement de l’écran et suppression du message d’avertissement

La suppression du message doit se faire dès que le joueur a basculé son écran en mode paysage. Vous devez donc détecter ce changement de position.

Et là miracle!!! Il existe un événement relatif à ce changement: orientationchange.

Il suffit donc d’appeler sur cet événement une fonction qui fait disparaitre le message, ici en l’occurrence le bloc screenPositionMessage.

Souvenez vous que tout ce qui est dédié aux contrôles ou aux interactions relève de la responsabilité du namespace game.control.

C’est donc à cet endroit que vous ajoutez la fonction appelée sur l’événement orientationchange.

game.control = {
  ....
  onOrientationChange : function() {
  }
}

Cette même fonction supprime le message en ayant remis les compteurs de résolution à zéro et sans lesquelles la vérification préalable d’un mode paysage effective serait vaine.

game.control = {
  ....
  onOrientationChange : function() {
    game.initScreenRes();
    if ( game.display.getScreenPosition() == "LANDSCAPE" ) {
      game.screenPositionMessage.style.display = "none";
    }
  }
}

Maintenant faites en sorte que cette fonction soit appelée sur l’événement orientationchange. Et là ça se passe dans le namespace game.

var game = {
  ....
  init : function() {
    ....
    this.initScreenMessage();	
    ....
  },
  ....
  initScreenMessage : function() {
    window.onorientationchange = game.control.onOrientationChange;
  }, 
}

Le tour est joué ? Hé bien non. Tous les calculs relatifs aux redimensionnement sont faits au lancement. Or si le lancement est fait en mode portrait, comme vous pouvez le voir en testant, vous obtenez un résultat pourri.

La simple disparition du message n’affecte en rien le dimensionnement qui est donc à faire lorsque que le basculement en mode paysage est effectif et uniquement à ce moment là.

Et c’est la fonction onOrientationChange qui va faire le taf, en fait tout simplement appeler les fonctions issues du refactoring resizeDisplayData,resizeHtmlElement,initEvent du namespace game.

game.control = {
  ....
  onOrientationChange : function() {
    game.initScreenRes();
    if ( game.display.getScreenPosition() == "LANDSCAPE" ) {
      game.screenPositionMessage.style.display = "none";
      game.resizeDisplayData(conf,game.ratioResX,game.ratioResX);
      game.resizeHtmlElement();
      game.initEvent();
    }
  }
}

Ces 3 fonctions sont appelées uniquement là et nulle part ailleurs. Les appels à ces fonctions doivent donc être retirés de la fonction init qui, du coup, s’allège encore.

var game = {
  ....
  init : function() {

    this.initScreenRes();
	
    this.resizeMessageElement();
    this.initScreenMessage();	

    this.initHtmlElement();	
	
    this.wallSound = new Audio("./sound/wall.ogg");
    this.playerSound = new Audio("./sound/player.ogg");

    game.ai.setPlayerAndBall(this.playerTwo, this.ball);
    game.speedUpBall();
	
    if ( game.display.getScreenPosition() == "PORTRAIT" ) {
      this.screenPositionMessage.style.display = "block";
    } else {
      this.resizeHtmlElement();
      this.initEvent()
    }
  },
  ....
}

Pour parfaire les choses, vous pouvez rendre visible le bloc blocToCenter au basculement en mode paysage (en passant le style display à la valeur block) et le faire disparaitre au basculement en mode portrait (en passant le style display à la valeur none).

Même chose pour ce qui est du message d’avertissement.

game.control = {
  ....
  onOrientationChange : function() {
    game.initScreenRes();
    if ( game.display.getScreenPosition() == "LANDSCAPE" ) {
      game.screenPositionMessage.style.display = "none";
      game.blocToCenter.style.display = "block";
      game.resizeDisplayData(conf,game.ratioResX,game.ratioResX);
      game.resizeHtmlElement();
      game.initEvent();
    } else {
      game.screenPositionMessage.style.display = "block";
      game.blocToCenter.style.display = "none";
    }
  },
  ....
}

 

Pour terminer

L’affichage de ces blocs est géré à 2 moments distincts:
– au démarrage du jeu par la position de l’écran du joueur: portrait ou paysage;
– au basculement de l’écran du jeu.

Ainsi, tout comme le bloc screenPositionMessage est masqué au démarrage, faites de même pour le bloc blocToCenter.

Ces 2 blocs sont antagonistes: ils ne peuvent s’afficher en même temps.

Masquez le bloc blocToCenter au démarrage en valorisant le style display à none dans le fichier source html pong.html.

<div id="blocToCenter" style="width:1010px;height:400px;position:absolute;left:50%;top:50%;margin:-200px 0 0 -505px;display:none;">
....
</div>

Au démarrage, jusqu’à présent vous gérez l’affichage du bloc screenPositionMessage lorsque que le jeu démarre en mode portrait. Faites de même avec le bloc blocToCenter lorsque le jeu démarre en mode paysage.

var game = {
  ....
  init : function() {

    this.initScreenRes();
    ....
    if ( game.display.getScreenPosition() == "PORTRAIT" ) {
      this.screenPositionMessage.style.display = "block";
    } else {
      this.blocToCenter.style.display = "block";
      this.resizeHtmlElement();
      this.initEvent()
    }
  },
  ....
}

Si vous testez en l’état, ça fonctionne si vous basculez une première fois en mode paysage. Si vous le faites une seconde fois, ça va coincer avec un affichage chaotique.

La cause de cela est le redimensionnement à chaque basculement en mode paysage. En fait le redimensionnement ne devrait être fait qu’une seule fois.

Ajoutez une nouvelle propriété resized à l’objet game indiquant si oui ou non un redimensionnement à été fait.

var game = {
  ....
  resized : false,
  ....
}

Au démarrage, il n’y a pas encore eu de redimensionnement, il est donc logique qu’elle soit valorisée à false.

Cette propriété est à valoriser à true lorsque les fonctions resizeDisplayData, resizeHtmlElement, et initEvent sont appelées une première fois. De façon à ce qu’elle empêche leurs ré-exécution.

Ainsi vous devez ajouter une clause if testant la valeur de game.resized: si game.resized est valorisée à false alors exécutez les 3 fonctions ci-dessus et fixée game.resized à la valeur true.

Les 3 fonctions resizeDisplayData, resizeHtmlElement, et initEvent sont appelées à 2 endroits distincts:
– depuis le fonction game.init;
– depuis le fonction game.control.onOrientationChange.

Ajoutez la clause à ces 2 endroits.

Depuis game.js:

var game = {
  ....
  init : function() {

    this.initScreenRes();
	
    this.resizeMessageElement();
    this.initScreenMessage();	

    this.initHtmlElement();	
	
    this.wallSound = new Audio("./sound/wall.ogg");
    this.playerSound = new Audio("./sound/player.ogg");

    game.ai.setPlayerAndBall(this.playerTwo, this.ball);
    game.speedUpBall();
	
    if ( game.display.getScreenPosition() == "PORTRAIT" ) {
      this.screenPositionMessage.style.display = "block";
    } else {
      this.blocToCenter.style.display = "block";
      if ( !this.resized ) {
        this.resizeDisplayData(conf,game.ratioResX,game.ratioResX);
        this.resizeHtmlElement();
        this.initEvent();
	this.resized = true;
      }
    }
  },
  ....
}

Depuis game.control.js:

game.control = {
  ....
  onOrientationChange : function() {
    game.initScreenRes();
    if ( game.display.getScreenPosition() == "LANDSCAPE" ) {
      game.screenPositionMessage.style.display = "none";
      game.blocToCenter.style.display = "block";
      if ( !game.resized ) {
        game.resizeDisplayData(conf,game.ratioResX,game.ratioResX);
        game.resizeHtmlElement();
        game.initEvent();
        game.resized = true;
      }
    } else {
      game.screenPositionMessage.style.display = "block";
      game.blocToCenter.style.display = "none";
      game.resizeMessageElement();
      game.initScreenMessage();
    }
  },
  ....
}

 

En guide de conclusion

Franchement (ce n’est pas une farce) et expérimentalement (https://developer.mozilla.org/en-US/docs/Web/API/Screen/lockOrientation ou https://www.w3.org/TR/screen-orientation/) il y a plus simple. Mais je ne vous ai pas fait bosser pour rien. Dans le cas présent, je voulais aussi vous montrer qu’il est possible d’arriver à un résultat lorsque les apis ne proposent pas de solution toute faite.

 
Pour voir en live le code et de préférence en testant à partir d’une tablette ou d’un smartphone, cliquez sur Pong

 

Ça vous a aidé, partagez, commentez sans hésitation.

 

Si vous constatez des coquilles, ou avez des remarques, une autre solution ou encore souhaitez manifester votre satisfaction de ce tuto, commentez.

 

Si vous avez besoin d’aide, contactez moi toujours sans hésiter.

 

Posté dans html5, pongTaggé canvas html5, canvas image, jeu video html5, tuto html5  |  Laisser un commentaire

Répondre