Android Gaming
|
Le développement d'un jeu basé sur les sprites en Android
demande un temps d'adaptation. Le cursus habituel quel que soit
le système d'exploitation utilisé consiste à :
Dans ce cycle, il n'y a pas de place pour la gestion du système,
l'interruption en cas d'appel téléphonique, la programmation
d'interface graphique standard Fenêtre - Icones - Menu - Souris.
Hors la programmation Android est par défaut basé sur ce type d'interface
WIMP
|
|
Quick Start
Dans ce tutoriel seront décrits
- La construction d'une interface android plein écran
- L'initialisation d'une surface graphique modifiable
- L'initialisation d'un thread de gestion, le plus rapide possible
- Le dessin du jeu frame après frame
- La sortie d'application
Interface Fullscreen
|
La 1ère étape consiste à forcer l'application complète à s'exécuter
en plein écran. Dans le descripteur AndroidManifest.xml, cela se
matérialise par le thème donné à l'activité principale de l'application :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.migniot.android.macgrowl" android:versionName="1.0" android:versionCode="9"> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".Game" android:label="@string/app_name" android:screenOrientation="landscape" android:theme="@android:style/Theme.NoTitleBar.Fullscreen"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> |
|
Surface Custom
Maintenant il faut obtenir une surface pour dessiner sur la totalité
de l'écran. La classe SurfaceView
est une View
dédiée à la gestion manuelle. Elle permet de dessiner des Bitmap
à la main à n'importe quel moment en s'affranchissant des contraintes
de rafraichissement standard.
SurfaceView ne sera pas utilisable directement. Créer une sous-classe
permet d'obtenir le controle du redimensionnement et d'initialiser,
de démarrer et stopper le Thread du jeu lors de l'affichage, de
la restauration et de la suspension de l'application. Ci-dessous
le layout déclarant une SurfaceView custom :
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">
<com.migniot.android.macgrowl.MacGrowlView
android:id="@+id/screen" android:focusableInTouchMode="true"
android:focusable="true" android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">
<com.migniot.android.macgrowl.MacGrowlView
android:id="@+id/screen" android:focusableInTouchMode="true"
android:focusable="true" android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
Il faut maintenant créer cette classe fille de SurfaceView.
Les méthodes interessantes de la classe sont :
Ci-dessous un extrait simple de la classe :
com.migniot.android.macgrowl.MacGrowlView.surfaceCreated(SurfaceHolder)
notifie de l'allocation de la surfacecom.migniot.android.macgrowl.MacGrowlView.surfaceChanged(SurfaceHolder, int, int, int)
notifie du dimensionnement initial de la surface, qui possède des dimensions nulles à sa créationcom.migniot.android.macgrowl.MacGrowlView.surfaceDestroyed(SurfaceHolder)
notifie de la destruction de la surface à la fermeture de l'application
Ci-dessous un extrait simple de la classe :
|
package com.migniot.android.macgrowl;
// [...] /** * The bitmap game surface. */ public class MacGrowlView extends SurfaceView implements Callback { /** * The game thread. */ private GameThread thread; /** * Constructor. * * @param context * The context * @param attrs * The attributes */ public MacGrowlView(Context context, AttributeSet attrs) { super(context, attrs); // [...] SurfaceHolder holder = getHolder(); holder.addCallback(this); setFocusable(true); } /** * Surface ready to serve. * * @param holder * The surface holder */ public void surfaceCreated(SurfaceHolder holder) { Log.d("mg", "Surface created"); } /** * Injects surface size. */ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Log.d("mg", "Surface changed, width = [" + width + "], height = [" + height + "]"); } /** * Inform of surface deletion */ public void surfaceDestroyed(SurfaceHolder holder) { Log.d("mg", "Surface destroyed"); } } |
|
Game Threading
Avant de continuer, on peut noter que le log capturé montre
un FPS
de 2. Cette vitesse d'animation est ridiculement basse et
ne permet pas de réaliser un jeu fluide. Rassurez-vous, cette
vitesse n'est obtenue que dans l'émulateur et l'exécution
sur un téléphone est satisfaisante - FPS = [52] sur un HTC Desire.
Il s'agit maintenant de lancer un Thread. Ce Thread est le coeur
du jeu, il se charge de :
- Redéssiner l'aire de jeu à partir d'une matrice
- Redéssiner les sprites
- Interroger l'état du clavier
- Modifier les coordonnées des sprites en conséquence
Ci-dessous la classe SurfaceView modifiée pour créer
le Thread :
|
package com.migniot.android.macgrowl;
import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.SurfaceHolder.Callback; /** * The bitmap game surface. */ public class MacGrowlView extends SurfaceView implements Callback { /** * The game thread. */ private GameThread thread; /** * Constructor. * * @param context * The context * @param attrs * The attributes */ public MacGrowlView(Context context, AttributeSet attrs) { super(context, attrs); SurfaceHolder holder = getHolder(); this.thread = new GameThread(holder, context); holder.addCallback(this); setFocusable(true); } /** * Surface ready to serve. * * @param holder * The surface holder */ public void surfaceCreated(SurfaceHolder holder) { Log.d("mg", "Surface created"); this.thread.setRunning(true); this.thread.start(); } /** * Injects surface size. */ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Log.d("mg", "Surface changed, width = [" + width + "], height = [" + height + "]"); this.thread.setSurfaceSize(width, height); } /** * Inform of surface deletion */ public void surfaceDestroyed(SurfaceHolder holder) { Log.d("mg", "Surface destroyed"); thread.setRunning(false); boolean alive = true; while (alive) { try { thread.join(); alive = false; } catch (InterruptedException e) { } } } } |
|
Frame Looping
La classe de Thread modifiée pour redessiner le tableau
à chaque itération :
package com.migniot.android.macgrowl;
import java.util.Random;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.Log;
import android.view.SurfaceHolder;
/**
* The game main loop.
*/
public class GameThread extends Thread {
/** The surface width. */
private int width;
/** The surface height. */
private int height;
/** The running state. */
private boolean running;
/** The surface holder. */
private SurfaceHolder holder;
// [...]
/** The context. */
private Context context;
/** The tiles. */
private Bitmap tiles;
/** The level. */
private static final int[][] level = {
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,4,5,5,5,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,4,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,4,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,5,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,4,5,5,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,16,17,19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,7,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,7,7,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,7,7,7,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,7,7,7,7,7,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3},};
/**
* Constructor.
*
* @param holder
* The surface holder
* @param context
* The context
*/
public GameThread(SurfaceHolder holder, Context context) {
this.holder = holder;
this.context = context;
this.tiles = BitmapFactory.decodeResource(this.context.getResources(),
R.drawable.tiles);
this.src = new Rect(0, 0, 16, 16);
this.dst = new Rect(0, 0, 16, 16);
this.fps = 0;
}
/**
* Injects the surface size.
*
* @param width
* The width
* @param height
* The height
*/
public void setSurfaceSize(int width, int height) {
synchronized (this.holder) {
this.width = width;
this.height = height;
}
}
/**
* Set the running state.
*
* @param running
* The state
*/
public void setRunning(boolean running) {
this.running = running;
}
/**
* Game main loop.
*/
@Override
public void run() {
long start = System.currentTimeMillis();
long count = 0;
Log.d("mg", "Game thread started");
while (running) {
updateState();
Canvas canvas = null;
try {
canvas = holder.lockCanvas(null);
synchronized (this.holder) {
draw(canvas);
}
count++;
} finally {
if (canvas != null) {
holder.unlockCanvasAndPost(canvas);
}
}
}
long end = System.currentTimeMillis();
this.fps = (1000 * count) / (end - start);
Log.d("mg", "FPS = [" + this.fps + "], count = [" + count
+ "], duration = [" + (end - start) + "]");
Log.d("mg", "Game thread ended");
}
/**
* Update the game state.
*/
private void updateState() {
}
/**
* Draw bitmaps.
*
* @param canvas
* The canvas
*/
private void draw(Canvas canvas) {
int[][] current = level;
src.top = src.left = dst.top = dst.left = 0;
src.bottom = src.right = dst.bottom = dst.right = 16;
for (int y = 0; y < current.length; y++) {
int[] line = current[y];
dst.top = y * 16;
dst.bottom = dst.top + 16;
dst.left = 0;
dst.right = 16;
for (int x = 0; x < line.length; x++) {
int tile = line[x];
src.left = tile * 17;
src.right = src.left + 16;
canvas.drawBitmap(this.tiles, src, dst, this.blackPaint);
dst.left += 16;
dst.right += 16;
}
}
}
/**
* Return the FPS.
*
* @return The FPS
*/
public long getFps() {
return fps;
}
}
import java.util.Random;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.Log;
import android.view.SurfaceHolder;
/**
* The game main loop.
*/
public class GameThread extends Thread {
/** The surface width. */
private int width;
/** The surface height. */
private int height;
/** The running state. */
private boolean running;
/** The surface holder. */
private SurfaceHolder holder;
// [...]
/** The context. */
private Context context;
/** The tiles. */
private Bitmap tiles;
/** The level. */
private static final int[][] level = {
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,4,5,5,5,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,4,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,4,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,5,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,4,5,5,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,16,17,19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,7,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,7,7,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,7,7,7,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,7,7,7,7,7,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3},};
/**
* Constructor.
*
* @param holder
* The surface holder
* @param context
* The context
*/
public GameThread(SurfaceHolder holder, Context context) {
this.holder = holder;
this.context = context;
this.tiles = BitmapFactory.decodeResource(this.context.getResources(),
R.drawable.tiles);
this.src = new Rect(0, 0, 16, 16);
this.dst = new Rect(0, 0, 16, 16);
this.fps = 0;
}
/**
* Injects the surface size.
*
* @param width
* The width
* @param height
* The height
*/
public void setSurfaceSize(int width, int height) {
synchronized (this.holder) {
this.width = width;
this.height = height;
}
}
/**
* Set the running state.
*
* @param running
* The state
*/
public void setRunning(boolean running) {
this.running = running;
}
/**
* Game main loop.
*/
@Override
public void run() {
long start = System.currentTimeMillis();
long count = 0;
Log.d("mg", "Game thread started");
while (running) {
updateState();
Canvas canvas = null;
try {
canvas = holder.lockCanvas(null);
synchronized (this.holder) {
draw(canvas);
}
count++;
} finally {
if (canvas != null) {
holder.unlockCanvasAndPost(canvas);
}
}
}
long end = System.currentTimeMillis();
this.fps = (1000 * count) / (end - start);
Log.d("mg", "FPS = [" + this.fps + "], count = [" + count
+ "], duration = [" + (end - start) + "]");
Log.d("mg", "Game thread ended");
}
/**
* Update the game state.
*/
private void updateState() {
}
/**
* Draw bitmaps.
*
* @param canvas
* The canvas
*/
private void draw(Canvas canvas) {
int[][] current = level;
src.top = src.left = dst.top = dst.left = 0;
src.bottom = src.right = dst.bottom = dst.right = 16;
for (int y = 0; y < current.length; y++) {
int[] line = current[y];
dst.top = y * 16;
dst.bottom = dst.top + 16;
dst.left = 0;
dst.right = 16;
for (int x = 0; x < line.length; x++) {
int tile = line[x];
src.left = tile * 17;
src.right = src.left + 16;
canvas.drawBitmap(this.tiles, src, dst, this.blackPaint);
dst.left += 16;
dst.right += 16;
}
}
}
/**
* Return the FPS.
*
* @return The FPS
*/
public long getFps() {
return fps;
}
}
Dans ce contexte on obtient une vitesse de 50 FPS sur un
HTC Desire. Ce taux de rafraichissement est suffisant
pour la plupart des jeux. Pour des jeux plus rapides
se reporter à l'excellent article
A Quick Primer du développeur de
Replica Island ou à une librairie, par exemple
andengine


11°