7 avril 2015 · 9 min de lecture
Nous allons disséquer un jeu vidéo et créer les bases d’une application Android en 2D.
Il s’agit d’un jeu “graphique”, c’est-à-dire un jeu “vidéo” dans le vrai sens du terme. Cela signifie que des images se succèdent pour créer un effet cinématique. On doit donc définir la fréquence des images par seconde (ou FPS en anglais, pour Frames Per Second). Le code qui gère l’affichage est une boucle permanente, dans laquelle on va à chaque fois :
Je suggère Android Studio qui est gratuit. Il contient tous les outils nécessaires pour développer une application Android : SDK, émulateurs… Si ce n’est déjà fait, créez un émulateur en allant dans le menu TOOLS - ANDROID - AVD MANAGER
.
Pour l’exemple, je vous propose de :
Dans Android Studio, créez un nouveau projet, avec une “blank activity”, c’est-à-dire un écran vierge.
Afin de vous faciliter la suite de la lecture, j’ai rédigé les explications directement dans le code, en commentaires.
Sur Android, il faut savoir que la boucle qui gère l’affichage vidéo doit être exécutée dans un processus distinct de l’application principale (on dit un thread). Nous l’appellerons GameLoopThread
.
Dans l’onglet “project”, “app”, “java”, faite un clic droit sur le package de votre application et choisissez “new - java class”. Nommez-le fichier “GameLoopThread” et cliquez sur OK. Copiez-collez le code suivant :
import android.graphics.Canvas;
public class GameLoopThread extends Thread
{
// on définit arbitrairement le nombre d'images par secondes à 30
private final static int FRAMES_PER_SECOND = 30;
// si on veut X images en 1 seconde, soit en 1000 ms,
// on doit en afficher une toutes les (1000 / X) ms.
private final static int SKIP_TICKS = 1000 / FRAMES_PER_SECOND;
private final GameView view; // l'objet SurfaceView que nous verrons plus bas
private boolean running = false; // état du thread, en cours ou non
// constructeur de l'objet, on lui associe l'objet view passé en paramètre
public GameLoopThread(GameView view) {
this.view = view;
}
// défini l'état du thread : true ou false
public void setRunning(boolean run) {
running = run;
}
// démarrage du thread
@Override
public void run()
{
// déclaration des temps de départ et de pause
long startTime;
long sleepTime;
// boucle tant que running est vrai
// il devient faux par setRunning(false), notamment lors de l'arrêt de l'application
// cf : surfaceDestroyed() dans GameView.java
while (running)
{
// horodatage actuel
startTime = System.currentTimeMillis();
// mise à jour du déplacement des ojets dans GameView.update()
synchronized (view.getHolder()) {view.update();}
// Rendu de l'image, tout en vérrouillant l'accès car nous
// y accédons à partir d'un processus distinct
Canvas c = null;
try {
c = view.getHolder().lockCanvas();
synchronized (view.getHolder()) {view.doDraw(c);}
}
finally
{
if (c != null) {view.getHolder().unlockCanvasAndPost(c);}
}
// Calcul du temps de pause, et pause si nécessaire
// afin de ne réaliser le travail ci-dessus que X fois par secondes
sleepTime = SKIP_TICKS-(System.currentTimeMillis() - startTime);
try {
if (sleepTime >= 0) {sleep(sleepTime);}
}
catch (Exception e) {}
} // boucle while (running)
} // public void run()
} // class GameLoopThread
Ajoutons une baballe, telle que définie dans Balle.java
:
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
class Balle
{
private BitmapDrawable img=null; // image de la balle
private int x,y; // coordonnées x,y de la balle en pixel
private int balleW, balleH; // largeur et hauteur de la balle en pixels
private int wEcran,hEcran; // largeur et hauteur de l'écran en pixels
private boolean move = true; // 'true' si la balle doit se déplacer automatiquement, 'false' sinon
// pour déplacer la balle on ajoutera INCREMENT à ses coordonnées x et y
private static final int INCREMENT = 5;
private int speedX=INCREMENT, speedY=INCREMENT;
// contexte de l'application Android
// il servira à accéder aux ressources, dont l'image de la balle
private final Context mContext;
// Constructeur de l'objet "Balle"
public Balle(final Context c)
{
x=0; y=0; // position de départ
mContext=c; // sauvegarde du contexte
}
// on attribue à l'objet "Balle" l'image passée en paramètre
// w et h sont sa largeur et hauteur définis en pixels
public BitmapDrawable setImage(final Context c, final int ressource, final int w, final int h)
{
Drawable dr = c.getResources().getDrawable(ressource);
Bitmap bitmap = ((BitmapDrawable) dr).getBitmap();
return new BitmapDrawable(c.getResources(), Bitmap.createScaledBitmap(bitmap, w, h, true));
}
// retourne 'true' si la balle se déplace automatiquement
// 'false' sinon
// car on la bloque sous le doigt du joueur lorsqu'il la déplace
public boolean isMoving() {
return move;
}
// définit si oui ou non la balle doit se déplacer automatiquement
// car on la bloque sous le doigt du joueur lorsqu'il la déplace
public void setMove(boolean move) {
this.move = move;
}
// redimensionnement de l'image selon la largeur/hauteur de l'écran passés en paramètre
public void resize(int wScreen, int hScreen) {
// wScreen et hScreen sont la largeur et la hauteur de l'écran en pixel
// on sauve ces informations en variable globale, car elles serviront
// à détecter les collisions sur les bords de l'écran
wEcran=wScreen;
hEcran=hScreen;
// on définit (au choix) la taille de la balle à 1/5ème de la largeur de l'écran
balleW=wScreen/5;
balleH=wScreen/5;
img = setImage(mContext,R.mipmap.ball,balleW,balleH);
}
// définit la coordonnée X de la balle
public void setX(int x) {
this.x = x;
}
// définit la coordonnée Y de la balle
public void setY(int y) {
this.y = y;
}
// retourne la coordonnée X de la balle
public int getX() {
return x;
}
// retourne la coordonnée Y de la balle
public int getY() {
return y;
}
// retourne la largeur de la balle en pixel
public int getBalleW() {
return balleW;
}
// retourne la hauteur de la balle en pixel
public int getBalleH() {
return balleH;
}
// déplace la balle en détectant les collisions avec les bords de l'écran
public void moveWithCollisionDetection()
{
// si on ne doit pas déplacer la balle (lorsqu'elle est sous le doigt du joueur)
// on quitte
if(!move) {return;}
// on incrémente les coordonnées X et Y
x+=speedX;
y+=speedY;
// si x dépasse la largeur de l'écran, on inverse le déplacement
if(x+balleW > wEcran) {speedX=-INCREMENT;}
// si y dépasse la hauteur l'écran, on inverse le déplacement
if(y+balleH > hEcran) {speedY=-INCREMENT;}
// si x passe à gauche de l'écran, on inverse le déplacement
if(x<0) {speedX=INCREMENT;}
// si y passe à dessus de l'écran, on inverse le déplacement
if(y<0) {speedY=INCREMENT;}
}
// on dessine la balle, en x et y
public void draw(Canvas canvas)
{
if(img==null) {return;}
canvas.drawBitmap(img.getBitmap(), x, y, null);
}
} // public class Balle
Ajoutez le fichier GameView.java
à votre projet. Il s’agit de l’affichage principal du jeu :
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
// SurfaceView est une surface de dessin.
// référence : https://developer.android.com/reference/android/view/SurfaceView
public class GameView extends SurfaceView implements SurfaceHolder.Callback {
// déclaration de l'objet définissant la boucle principale de déplacement et de rendu
private GameLoopThread gameLoopThread;
private Balle balle;
// création de la surface de dessin
public GameView(Context context) {
super(context);
getHolder().addCallback(this);
gameLoopThread = new GameLoopThread(this);
// création d'un objet "balle", dont on définira la largeur/hauteur
// selon la largeur ou la hauteur de l'écran
balle = new Balle(this.getContext());
}
// Fonction qui "dessine" un écran de jeu
public void doDraw(Canvas canvas) {
if(canvas==null) {return;}
// on efface l'écran, en blanc
canvas.drawColor(Color.WHITE);
// on dessine la balle
balle.draw(canvas);
}
// Fonction appelée par la boucle principale (gameLoopThread)
// On gère ici le déplacement des objets
public void update() {
balle.moveWithCollisionDetection();
}
// Fonction obligatoire de l'objet SurfaceView
// Fonction appelée immédiatement après la création de l'objet SurfaceView
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
// création du processus GameLoopThread si cela n'est pas fait
if(gameLoopThread.getState()==Thread.State.TERMINATED) {
gameLoopThread=new GameLoopThread(this);
}
gameLoopThread.setRunning(true);
gameLoopThread.start();
}
// Fonction obligatoire de l'objet SurfaceView
// Fonction appelée juste avant que l'objet ne soit détruit.
// on tente ici de stopper le processus de gameLoopThread
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
boolean retry = true;
gameLoopThread.setRunning(false);
while (retry) {
try {
gameLoopThread.join();
retry = false;
}
catch (InterruptedException e) {}
}
}
// Gère les touchés sur l'écran
@Override
public boolean onTouchEvent(MotionEvent event) {
int currentX = (int)event.getX();
int currentY = (int)event.getY();
switch (event.getAction()) {
// code exécuté lorsque le doigt touche l'écran.
case MotionEvent.ACTION_DOWN:
// si le doigt touche la balle :
if(currentX >= balle.getX() &&
currentX <= balle.getX()+balle.getBalleW() &&
currentY >= balle.getY() && currentY <= balle.getY()+balle.getBalleH() ) {
// on arrête de déplacer la balle
balle.setMove(false);
}
break;
// code exécuté lorsque le doight glisse sur l'écran.
case MotionEvent.ACTION_MOVE:
// on déplace la balle sous le doigt du joueur
// si elle est déjà sous son doigt (oui si on a setMove à false)
if(!balle.isMoving()) {
balle.setX(currentX);
balle.setY(currentY);
}
break;
// lorsque le doigt quitte l'écran
case MotionEvent.ACTION_UP:
// on reprend le déplacement de la balle
balle.setMove(true);
}
return true; // On retourne "true" pour indiquer qu'on a géré l'évènement
}
// Fonction obligatoire de l'objet SurfaceView
// Fonction appelée à la CREATION et MODIFICATION et ONRESUME de l'écran
// nous obtenons ici la largeur/hauteur de l'écran en pixels
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int w, int h) {
balle.resize(w,h); // on définit la taille de la balle selon la taille de l'écran
}
} // class GameView
Enfin, modifiez le code de MainActivity.java
comme ceci :
import android.app.Activity;
import android.os.Bundle;
public class MainActivity extends Activity {
private GameView gameView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// On cré un objet "GameView" qui est le code principal du jeu
gameView=new GameView(this);
// et on l'affiche.
setContentView(gameView);
}
} // class MainActivity
Dans l’un des dossiers mipmap
, ajoutez l’image de la balle, par exemple dans app/src/main/res/mipmap-hdpi/
.
Lancez l’application avec le bouton “lecture” (ou MAJ+F10).
Vous verrez une balle de tennis se déplacer à l’écran et rebondir sur les bords. A tout moment vous pouvez la déplacer avec le doigt.
Vous avez maintenant toutes les bases pour développer votre jeu Android 2D !