Stein-Schere-Papier Beispielanwendung

Eine kleine Beispielanwendung in Android, die zeigt, wie man effektiv mit 2D Grafiken arbeiten kann, gibt es im folgenden. Das Beispielprogramm ist eine Abwandlung von Stein-Schere-Papier.

Die folgenden Funktionen sind implementiert:

  • Erstelle ein Stein, eine Schere oder Papier per Berührung
  • Lege Bewegungsrichtung und Geschwindigkeit fest durch Touch-Drag-Release
  • Bei Kollision explodiert der Verlierer
  • Es gibt einen Explosionssound


Zuerst ein paar Bilder:
stone-scissor-paper

stone-scissor-paper

Diese kleine Anwendung hat 4 Klassen, darunter eine Activity, eine View, ein Thread und eine eigene Klasse, welche die Grafiken repräsentiert. Hier eine kleine Klassenstruktur zur besseren Übersicht:
RockScissorsPaper class diagram

Die Namen der Klassen, Variablen und Methoden ist so gewählt, dass es relativ einfach sein sollte, deren Sinn zu verstehen. Wir gehen nun durch jede Klasse durch und erklären kurz das Wichtigste.
Zuerst kommt die Activity namens RockScissorsPaper.
package com.droidnova.android.rockscissorspaper;

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

/**
* Activity which will be used as main entry point for the application.
*
* @author martin
*/
public class RockScissorsPaper extends Activity {

/**
* Method called on application start.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(new Panel(this));
}
}

Nur 2 Zeilen sind wichtig:
requestWindowFeature(Window.FEATURE_NO_TITLE); fordert den WindowManager auf, ein Fenster ohne Titelleiste zu erstellen.
setContentView(new Panel(this)); setzt eine Instanz unserer View Klasse namens Panel als Content.

Als nächstes die Grafiken. Die Klasse selbst heißt Graphic.
package com.droidnova.android.rockscissorspaper;

import android.graphics.Bitmap;

/**
* Class which contains a object we want to draw on specific position.
*
* @author martin
*/
public class Graphic {

/**
* Class which represent the speed the object has in both x and y direction.
*
* @author martin
*/
public class Speed {
private int _x = 1;
private int _y = 1;

/**
* @return The speed in x direction. Negative amount means, the speed is
* backward.
*/
public int getX() {
return _x;
}

/**
* @param speed Speed in x direction. Negative amount means, the speed
* is backward.
*/
public void setX(int speed) {
_x = speed;
}

/**
* @return The speed in y direction. Negative amount means, the speed is
* backward.
*/
public int getY() {
return _y;
}

/**
* @param speed Speed in y direction. Negative amount means, the speed
* is backward.
*/
public void setY(int speed) {
_y = speed;
}

/**
* Helper method. Useful for debugging.
*/
public String toString() {
return "Speed: x: " + _x + " | y: " + _y;
}
}

/**
* Contains the coordinates of the instance.
*
* @author martin
*/
public class Coordinates {
private int _x = 0;
private int _y = 0;

/**
* @return The x coordinate of the upper left corner.
*/
public int getX() {
return _x;
}

/**
* @return The x coordinate of the center.
*/
public int getTouchedX() {
return _x + _bitmap.getWidth() / 2;
}

/**
* @param value The new x coordinate of the upper left corner.
*/
public void setX(int value) {
_x = value;
}

/**
* @param value The new x coordinate of the center.
*/
public void setTouchedX(int value) {
_x = value - _bitmap.getWidth() / 2;
}

/**
* @return The y coordinate of the upper left corner.
*/
public int getY() {
return _y;
}

/**
* @return The y coordinate of the center.
*/
public int getTouchedY() {
return _y + _bitmap.getHeight() / 2;
}

/**
* @param value The new y coordinate of the upper left corner.
*/
public void setY(int value) {
_y = value;
}

/**
* @param value The new y coordinate of the center.
*/
public void setTouchedY(int value) {
_y = value - _bitmap.getHeight() / 2;
}

/**
* Helper method for debugging.
*/
public String toString() {
return "Coordinates: (" + _x + "/" + _y + ")";
}
}

/**
* Bitmap which should be drawn.
*/
private Bitmap _bitmap;

/**
* Coordinates on which the bitmap should be drawn.
*/
private Coordinates _coordinates;

/**
* Speed of the object.
*/
private Speed _speed;

/**
* Object type which could be rock, scissors, paper or explosion.
*/
private String _type;

/**
* Step of explosion which will take 50 steps.
*/
private int _explosionStep = 0;

/**
* Constructor.
*
* @param bitmap Bitmap which should be drawn.
*/
public Graphic(Bitmap bitmap) {
_bitmap = bitmap;
_coordinates = new Coordinates();
_speed = new Speed();
}

/**
* @param bitmap New bitmap to draw.
*/
public void setBitmap(Bitmap bitmap) {
_bitmap = bitmap;
}

/**
* @return The stored bitmap.
*/
public Bitmap getBitmap() {
return _bitmap;
}

/**
* @return The speed of the instance
*/
public Speed getSpeed() {
return _speed;
}

/**
* @return The coordinates of the instance.
*/
public Coordinates getCoordinates() {
return _coordinates;
}

/**
* @param type The new type of the instance.
*/
public void setType(String type) {
_type = type;
}

/**
* @return The type of the instance.
*/
public String getType() {
return _type;
}

/**
* @param step The new explosion step.
*/
public void setExplosionStep(int step) {
_explosionStep = step;
}

/**
* @return The explosion step.
*/
public int getExplosionStep() {
return _explosionStep;
}
}

Die Graphic Klasse speichert einige Informationen.
Die erste Information ist die Geschwindigkeit, die eine Grafik haben kann. Die Werte x und y sowie ihr Vorzeichen geben an, welche Richtung sie haben. Die Größe der Werte gibt an, wie viele Pixel in einem Spieldurchlauf die Grafik bewegt werden soll.
Die zweite Information ist die Koordinate, an der die Grafik sich befindet. Da die Koordinaten aber immer auf die obere linke Ecke der Grafik bezogen sind, muss sichergestellt werden, dass bei einem Klick auf die Oberfläche die Grafik dennoch zentriert unter dem Finger auftaucht.
Die letzte wichtige Information ist natürlich die Grafik selbst. Also ein Stein, oder eine Schere. Eine Explosion selbst läuft in 50 Schritten ab und wird stückweise durchlaufen.

Die Game-Loop ist in einem Thread realisiert. Die Klasse heißt RockScissorsPaperThread.
package com.droidnova.android.rockscissorspaper;

import android.graphics.Canvas;

/**
* Thread class to perform the so called "game loop".
*
* @author martin
*/
class RockScissorsPaperThread extends Thread {
private Panel _panel;
private boolean _run = false;

/**
* Constructor.
*
* @param panel View class on which we trigger the drawing.
*/
public RockScissorsPaperThread(Panel panel) {
_panel = panel;
}

/**
* @param run Should the game loop keep running?
*/
public void setRunning(boolean run) {
_run = run;
}

/**
* @return If the game loop is running or not.
*/
public boolean isRunning() {
return _run;
}

/**
* Perform the game loop.
* Order of performing:
* 1. update physics
* 2. check for winners
* 3. draw everything
*/
@Override
public void run() {
Canvas c;
while (_run) {
c = null;
try {
c = _panel.getHolder().lockCanvas(null);
synchronized (_panel.getHolder()) {
_panel.updatePhysics();
_panel.checkForWinners();
_panel.onDraw(c);
}
} finally {
// do this in a finally so that if an exception is thrown
// during the above, we don't leave the Surface in an
// inconsistent state
if (c != null) {
_panel.getHolder().unlockCanvasAndPost(c);
}
}
}
}
}

Die Implementation ist relativ simple. Wir haben zwei Methoden, die eine setzt unsere Abbruchvariable. Die andere führt die eigentliche Spielelogik durch. Diese Methode ist das Herzstück der Anwendung. Hier wird die Canvas-Oberfläche gesperrt, es kann nun darauf gemalt werden. Danach wird sie entsperrt und in den Frontbuffer kopiert um angezeigt zu werden.
Synchronisierte mit der View wird die Spielephysik aufgerufen, die Gewinner einer Kollision ermittelt und danach die Spielfläche gemalt.

Die Panelklasse ist die Klasse, die für alles wichtige zuständig ist. Sie ist etwa 400 Zeilen lang und komplett kommentiert. Deshalb wird nur das interessante und wichtige hier erklärt.
Starten wir mit dem Konstruktor:
/**
* Constructor called on instantiation.
* @param context Context of calling activity.
*/
public Panel(Context context) {
super(context);
fillBitmapCache();
_soundPool = new SoundPool(16, AudioManager.STREAM_MUSIC, 100);
_playbackFile = _soundPool.load(getContext(), R.raw.explosion, 0);
getHolder().addCallback(this);
_thread = new RockScissorsPaperThread(this);
setFocusable(true);
}

Zwei wichtige Dinge für die Soundwiedergabe sind hier zu sehen. Einmal wird ein Soundpool mit 16 Kanälen erstellt und der Explosionssound wird geladen.

/**
* Update the physics of each item already added to the panel.
* Not including items which are currently exploding and moved by a touch event.
*/
public void updatePhysics() {
Graphic.Coordinates coord;
Graphic.Speed speed;
for (Graphic graphic : _graphics) {
coord = graphic.getCoordinates();
speed = graphic.getSpeed();

// Direction
coord.setX(coord.getX() + speed.getX());
coord.setY(coord.getY() + speed.getY());

// borders for x...
if (coord.getX() getWidth()) {
speed.setX(-speed.getX());
coord.setX(coord.getX() + getWidth() - (coord.getX() + graphic.getBitmap().getWidth()));
}

// borders for y...
if (coord.getY() getHeight()) {
speed.setY(-speed.getY());
coord.setY(coord.getY() + getHeight() - (coord.getY() + graphic.getBitmap().getHeight()));
}
}
}

Hier werden Kollisionen mit dem Bildschirmrand überprüft und die Bilder werden auf ihre aktuelle Position verschoben.

/**
* Check all items on the panel for collisions and find the winner.
* The loser will added to the list of explosions.
*/
public void checkForWinners() {
ArrayList toExplosion = new ArrayList();
for (Graphic grapics : _graphics) {
for (Graphic battleGraphic : _graphics) {
if (battleGraphic != grapics && !(toExplosion.contains(battleGraphic) || toExplosion.contains(grapics))) {
if (!battleGraphic.getType().equals(grapics.getType()) && checkCollision(battleGraphic, grapics)) {
if (firstWins(battleGraphic.getType(), grapics.getType())) {
toExplosion.add(grapics);
_soundPool.play(_playbackFile, 1, 1, 0, 0, 1);
}
}
} else {
continue;
}
}
}
if (!toExplosion.isEmpty()) {
_explosions.addAll(toExplosion);
_graphics.removeAll(toExplosion);
}
}

Hier wird der Gewinner einer Kollision ermittelt. Wenn eine Kollision ermittelt wurde, wird der Verlierer durch eine Explosion ersetzt und der Sound für diese abgespielt.

Die onDraw() Methode, die alles malt ist etwas komplexer:
/**
* Draw on the SurfaceView.
* Order:
*

    *

  • Background image
  • *

  • Items on the panel
  • *

  • Explosions
  • *

  • Item moved by hand
  • *

*/
@Override
public void onDraw(Canvas canvas) {
// draw the background
canvas.drawBitmap(_bitmapCache.get(R.drawable.abstrakt), 0, 0, null);
Bitmap bitmap;
Graphic.Coordinates coords;
// draw the normal items
for (Graphic graphic : _graphics) {
bitmap = graphic.getBitmap();
coords = graphic.getCoordinates();
canvas.drawBitmap(bitmap, coords.getX(), coords.getY(), null);
}

// draw the explosions
ArrayList finishedExplosion = new ArrayList();
for (Graphic graphic : _explosions) {
if (!graphic.getType().equals("explosion")) {
graphic.setType("explosion");
graphic.setExplosionStep(0);
graphic.getSpeed().setX(0);
graphic.getSpeed().setY(0);
graphic.setBitmap(_bitmapCache.get(R.drawable.smaller));
bitmap = graphic.getBitmap();
coords = graphic.getCoordinates();
canvas.drawBitmap(bitmap, coords.getX(), coords.getY(), null);
} else {
switch (graphic.getExplosionStep()) {
case 10: bitmap = _bitmapCache.get(R.drawable.small);
graphic.setBitmap(bitmap);
break;
case 20: bitmap = _bitmapCache.get(R.drawable.big);
graphic.setBitmap(bitmap);
break;
case 30: bitmap = _bitmapCache.get(R.drawable.small);
graphic.setBitmap(bitmap);
break;
case 40: bitmap = _bitmapCache.get(R.drawable.smaller);
graphic.setBitmap(bitmap);
break;
default: bitmap = graphic.getBitmap();
}
coords = graphic.getCoordinates();
canvas.drawBitmap(bitmap, coords.getX(), coords.getY(), null);
graphic.setExplosionStep(graphic.getExplosionStep() + 1);
}
if (graphic.getExplosionStep() > 50) {
finishedExplosion.add(graphic);
}
}

// remove all Objects who are already fully exploded...
if (!finishedExplosion.isEmpty()) {
_explosions.removeAll(finishedExplosion);
}

// draw current graphic at last...
if (_currentGraphic != null) {
bitmap = _currentGraphic.getBitmap();
coords = _currentGraphic.getCoordinates();
canvas.drawBitmap(bitmap, coords.getX(), coords.getY(), null);
}
}

Als erstes wird das Hintergrundbild gemalt. Dann wird über die Liste aller Grafiken iteriert.
Der zweite Schritt ist die Abwicklung der Explosion. Eine Explosion besteht aus 50 Schritten, wobei alle 10 Schritte die Grafik verändert wird. Dadurch ist es möglich, dass die Explosion nicht zu schnell vorbei ist. Sind die 50 Schritte durchlaufen, wird die Explosion entfernt und das Objekt verworfen.
Am Ende wird dann die Grafik gezeichnet, die sich gerade durch eine Berührung unter unserem Finger befindet. Das ist alles.

Ich hoffe die kurze Einführung hat euch Spass gemacht und ihr konntet was lernen.

Quellcode (komplettes Eclipse Projekt): RockScissorsPaper

Leave a Reply

Your email address will not be published. Required fields are marked *