20 janv. 2012

Boucle de jeu sous Android

Tutoriel Développement Jeux sur Android Part 1 : boucle de jeu 
On entend souvent parler de la boucle de jeu. Il s’agit en fait d’une boucle quasi-infinie au cours de laquelle s’effectuent généralement trois tâches essentielles:
  • Traitement des entrées (input)
  • Mis à jour (update)
  • Affichage (render)
Un jeu basique est  composé d’une seule boucle, c’est la boucle d’affichage. Mais on peut également trouver d’autres boucles comme la lecture des fichiers audio ou des flux réseaux par exemple. Ainsi on dit d’un jeu est composé de plusieurs moteurs : moteur graphique, moteur audio, etc. Voir Utilité d'un moteur de jeu

Dans cette première partie de tutoriel consacré au développement de jeux sur Android, mon objectif est de vous montrer comment réaliser une animation simple en respectant l’architecture d’un jeu.  Vous allez voir qu’au fil du temps, cette architecture facilite beaucoup la réalisation de n’importe quelle application interactive sur Android.

Si vous êtes débutant en Android et que vous aurez des doutes sur les notions de bases (Activité, View), référez-vous à ce site.
Nous aurons besoin de deux classes seulement, en plus le point d’entrée qui est l’Activité:
Le composant GameView
Nous allons définir une vue spécifique pour notre activité en héritant de la classe SurfaceView. Cette dernière nous donne accès à son buffer (mémoire tampon pour dessiner nos objets) et aussi à des événements du type MotionEvent (toucher l’écran).
package com.creativegames;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Bitmap.Config;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;

public class GameView extends SurfaceView implements Callback{
 
 public int width; //largeur de l'écran
 public int height; //hauteur de l'écran 
 public Canvas canvas; //outil pour dessiner sur l'écran
 private Bitmap buffer; // pixel buffer 
 private SurfaceHolder holder;
 private GameLoop game; //pointeur vers la boucle de jeu
 
 public GameView(Context context, GameLoop game) {
  super(context);
  this.holder = getHolder();
  this.holder.addCallback(this);
  this.game = game;
 }
 
 /** Rafraichir l'écran*/
 @Override
 public void invalidate() {
  if (holder != null) {
   Canvas c = holder.lockCanvas();
   if (c != null) {
    c.drawBitmap(buffer, 0, 0, null);
    holder.unlockCanvasAndPost(c);
   }
  }
 }
 
 /**callback lorsque l'écran est touché
  * on stocke l'événement pour être ensuite traité dans la boucle de jeu*/
 @Override
 public boolean onTouchEvent(MotionEvent event) {
  this.game.lastEvent = event;
  return true;
 }

 /** callback lorsque la surface est chargée, 
  * donc démarrer la boucle de jeu*/
 public void surfaceChanged(SurfaceHolder holder, int format, 
   int width, int height) {
  this.width = width;
  this.height = height;
  this.buffer = Bitmap.createBitmap(width, height, Config.ARGB_8888);
  this.canvas = new Canvas(buffer);
  this.game.start();
 }

 public void surfaceCreated(SurfaceHolder holder) {
 }

 public void surfaceDestroyed(SurfaceHolder holder) {
 }
}


La classe GameLoop
Comme la boucle de jeu doit tourner indépendamment de la boucle d’affichage principale de l’activité, il faut lancer GameLoop comme un thread. Elle sera démarrée dès que la vue (GameView) sera prête.


package com.creativegames;

import com.creativegame.R;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.view.MotionEvent;

public class GameLoop extends Thread {

 /** Variable booléenne pour arrêter le jeu */
 public boolean running;

 /**
  * durée de la pause entre chaque frame 
         * du jeu pour frame per second FPS=10
  * on a sleepTime=100
  */
 private long sleepTime = 100;

 /** Ecran de jeu */
 public GameView screen;
 
 /** le dernier évenement enregistré sur l'écran*/
 public MotionEvent lastEvent; 

 /** Position de l'image que nous dessions sur l'écran */
 private int x, y;
 /** vitesse de l'image : nombre de pixel parcouru à chaque boucle de jeu */
 private int vx;
 /** image que nous allons dessiner */
 private Bitmap img;
 /** contexte de l'application */
 private Context context;
 /** activer ou désactiver l'animation*/
 private boolean animate;

 public void initGame(Context context) {
  this.context = context;
  img = ((BitmapDrawable) context.getResources().getDrawable(
    R.drawable.ic_launcher)).getBitmap();
  x = 0;
  y = 10;
  vx = 2; 
  animate = true;
  running = true;
  this.screen = new GameView(context, this);
 }

 /** la boucle de jeu */
 @Override
 public void run() {
  while (this.running) {
   this.processEvents();
   this.update();
   this.render();
   try {
    Thread.sleep(sleepTime);
   } catch (InterruptedException e) {
   }
  }
 }

 /** Dessiner les composant du jeu sur le buffer de l'écran*/
 public void render() {
  Paint paint = new Paint();
  paint.setColor(0xFF000000);
                //effacer l'écran avec la couleur noire
  this.screen.canvas.drawPaint(paint);
  this.screen.canvas.drawBitmap(img, x, y, null);
  this.screen.invalidate();
 }

 /** Mise à jour des composants du jeu
  *  Ici nous déplaçon le personnage avec la vitesse vx
  *  S'il sort de l'écran, on le fait changer de direction
  *  */
 public void update() {
  if(this.animate==false) return;
  int oldX = x;
  x = x + vx;
  if (x < 0 || x > screen.width - img.getWidth()) {
   x = oldX;
   vx = -vx;
  }
 }
 
 /** Ici on va faire en sorte que lorsqu'on clique sur l'écran,
  * L'animation s'arrête/redémarre
  * */
 public void processEvents() {
  if (lastEvent != null && 
                   lastEvent.getAction() == MotionEvent.ACTION_DOWN) {
   this.animate = ! this.animate;   
  }
  lastEvent = null;
 }
}


Le programme principal
Dans le point d’entrée de l’application qui est l’activité, il nous reste à instancier un objet GameLoop et affecter sa gameView à l’activité.
N’oubliez pas d’arrêter proprement la boucle de jeu lorsque l’application est fermée (onDestroy()).

package com.creativegames;

import android.app.Activity;
import android.os.Bundle;

public class Main extends Activity {
 
 private GameLoop game;
 
 /** A la création de l'activité
  * 1. on initialise le jeu et affecter la vue à l'activité
  * */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        game = new GameLoop();
        game.initGame(this);       
        setContentView(game.screen);
    }
    
    /** Lorsque l'application s'arrête, 
   il faut arrêter proprement la boucle de jeu*/
    @Override
    protected void onDestroy() {
     game.running = false;
     super.onDestroy();
    }
}
Voici me résultat de l'exécution du code: une image qui se déplace en va et vient dans l'écran, cadencé par le sleepTime. Au clic, l'animation s'arrête/redémarre.
Faiblesses de cette méthode
La fait de fixer le sleepTime suppose que la frame du jeu s’exécute pendant 0 ms. Ce qui n’est pratiquement  pas le cas. En effet si nous ajoutons beaucoup de traitements dans GameLoop.update(), le FPS va varier et notre jeu deviendra moins performant. La solution consiste donc à  ajuster le sleepTime à chaque frame.
Proposition de solution.

@Override
 public void run() {
  long startTime;
  long elapsedTime; // durée de (update()+render())
  long sleepCorrected; // sleeptime corrigé
  while (this.running) {
   startTime = System.currentTimeMillis();
   this.processEvents();
   this.update();
   this.render();
   elapsedTime = System.currentTimeMillis() - startTime;
   sleepCorrected = sleepTime - elapsedTime;
   // si jamais sleepCorrected<0 alors faire une pause de 1 ms
   if (sleepCorrected < 0) {
    sleepCorrected = 1;
   }
   try {
    Thread.sleep(sleepCorrected > 0 ? sleepCorrected : 1);
   } catch (InterruptedException e) {
   }
   // calculer le FSP
   fps = (int) (1000/(System.currentTimeMillis() - startTime));
  }
 }



Si vous avez des remarques ou erreurs à corriger ne manquez pas de m’informer via un commentaire.


4 commentaires:

Anonyme a dit…

Merci pour ces tutoriels. C'est vraiment très intéressant.

Fabien a dit…

Quand est ce que la méthode run() est elle appelée ? Je ne trouve pas...
Merci d'avance ;)

Fabien a dit…

Ah okay c'est bon j'ai pigé comme c'est hérité de Thread c'est appelé automatiquement ! Cimer albert :)

Unknown a dit…

Voici un excellent tuto pour mettre le pied à l'étrier... je vais tenter de m'y mettre aussi.
Btw as tu produit d'autres jeux dispo sur playstore ?