How-to : Een simpele Game Loop

Mijn aPpLeZ-Game is gebaseerd op een eenvoudige tutorial, Droidz . In de komende posts zal ik uitleggen hoe het raamwerk van de game eruit ziet en hoe je zelf een simpele game kunt bouwen. Daarnaast zal ik ingaan op de problemen die ik tegenkwam bij het ontwikkelen van Applez. Uiteindelijk heb ik, voor Applez, 12 vragen gesteld aan de StackOverflow community. Als je in die community even zoekt op mijn naam, kun je vrij makkelijk de vragen over Applez vinden.

Goed. Een game. Hoe werkt dat eigenlijk in Android? Je moet bedenken dat er sprake is van een eenvoudige architectuur. De basis is eigenlijk : Controleer gebruikersinput > Controleer plaatsen en gebeurtenissen van objecten (Physics) > Verwerk data > Teken het scherm > Speel geluid af. Daarna begint de loop weer opnieuw. Dat alles gaat zo snel, dat je erop moet rekenen dat je bij een simpel spelletje, zoals Applez, al snel 50 keer per seconde het scherm kunt tekenen (50 Frames per Second). In de tekening bij deze post kun je de beschreven architectuur herkennen.

Hieronder geef ik de code voor een simpele game-loop. CopPa (jep, CopyPaste 😉 ) maakt gebruik van een Background, een View, een Thread en een Activity. Als je deze op de juiste manier in je IDE hangt, heb je het begin van een game.  Je moet de background (backgnd_coppaview.png) wel even maken in je favo tekenpakket en dan in de directory res\drawable-mdpi (voor medium-density schermen) zetten. Je kunt objecten die niet veranderen opnemen in de achtergrond. Dat kunnen dus ook best knoppen (buttons) zijn. Later, in de code, vang je de onTouchEvents af. Op dat moment zal je met code kijken in welk gebied de touch precies was. Als dat binnen de kaders van de button (op je achtergrond) was, dan kun je een routine uitvoeren.

Okido. Daar gaat ie met de eerste Activity :

package happyworx.nl.coppa;

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

/**
 * @author k.koenen@happyworx.nl Gemaakt om een simpele game loop te
 *         demonstreren.
 */

public class CoppaActivity extends Activity {

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		// Verzoek om het de titelbalk van het scherm uit te zetten
		requestWindowFeature(Window.FEATURE_NO_TITLE);
		// Volledig scherm gebruiken
		getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
				WindowManager.LayoutParams.FLAG_FULLSCREEN);
		// De CoppaView class wordt de view die we gaan gebruiken
		// Als je advertenties aan je game wilt toevoegen,
		// zul je hier de trukendoos los moeten trekken.
		// Je maakt dan een new RelativeLayout en voegt
		// niet alleen de CoppaView, maar ook de AdView
		// daaraan toe.
		setContentView(new CoppaView(this));
	}
}

Zoals je ziet, wordt de ContentView gezet naar de CoppaView class. Dat is dan ook de volgende die we gaan bekijken;

package happyworx.nl.coppa;

import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.util.Log;
import android.view.Display;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.WindowManager;
import android.widget.Toast;

public class CoppaView extends SurfaceView implements SurfaceHolder.Callback {

	// Een variabele voor de Thread (die de gameloop uitvoert)
	private MainThread thread;
	// En eentje voor de achtergrond van deze view
	private BackGndManager backGnd;

	// Public uitleesbare variabelen voor grootte van het scherm. De RectScreen
	// vullen we later met de juiste dimensies. Deze RectScreen is nodig in de
	// BackGndManager om ervoor te zorgen dat de achtergrond netjes het hele
	// scherm van de telefoon vult.
	public static Rect RectScreen = new Rect(0, 0, 0, 0);
	public int height;
	public int width;

	// Als de CoppaView class gemaakt wordt in een andere class, dan wordt eerst
	// de routine hieronder uitgevoerd. (dus ook bij de setContentView =
	// (new..))
	public CoppaView(Context context) {
		super(context);

		// Een surface wordt in een surfaceholder vastgehouden. Om events te
		// kunnen
		// vangen, moeten we een addCallback aan de SurfaceHolder toevoegen.
		getHolder().addCallback(this);

		// Even een paar dingen die we nodig hebben om een achtergrond voor deze
		// view te definieren. Ik heb een BackGndManager gemaakt, die binnen de
		// view de back-ground voor zn rekening neemt. We voeren de
		// BackGndManager een bitmap en de BackGndManager beschikt zelf ook over
		// een onDraw-routine, zodat we kleine wijzigingen daar kunnen laten
		// aanbrengen. Geen idee of dit de meest praktische routine is,
		// voorlopig hanteer ik hem even. Eerst de eigenschappen van het display
		// uitlezen en opslaan in de variabelen.

		Display display = ((WindowManager) getContext().getSystemService(
				Context.WINDOW_SERVICE)).getDefaultDisplay();
		width = display.getWidth();
		height = display.getHeight();
		RectScreen.set(0, 0, width, height);

		// Nu de background voor deze view definieren.
		backGnd = new BackGndManager(BitmapFactory.decodeResource(
				getResources(), R.drawable.backgnd_coppaview), 0, 0);

		// We definieren een thread die de GameLoop zal uitvoeren.
		// Door dit te doen, zal de code in public MainThread... (in de
		// Mainthread class)
		// dus direct worden uitgevoerd!
		thread = new MainThread(getHolder(), this);

		// We maken de CoppaView 'focussable' zodat events kunnen worden
		// afgehandeld
		setFocusable(true);

	}

	public void surfaceCreated(SurfaceHolder holder) {
		// Hier weten we zeker dat het surface is gecreeerd
		// En kunnen we dus de thread starten

		thread.setRunning(true);
		thread.start();
	}

	public void surfaceChanged(SurfaceHolder holder, int format, int width,
			int height) {
		// Hier kun je code toevoegen die wordt uitgevoerd als het Surface is
		// veranderd. Volgens mij valt hier ook onder; als het surface opnieuw
		// wordt opgebouwd doordat bijvoorbeeld de gebruiker de telefoon heeft
		// gedraaid (naar andere screen-orientation)
	}

	public void surfaceDestroyed(SurfaceHolder holder) {
		// En hier code voor als het surface niet meer bestaat (applicatie wordt
		// afgesloten of de Activity die het surface houdt wordt door het
		// systeem beeindigd.
	}

	// In de routine hieronder vangen we de touch-events af voor de
	// CoppaView-class

	@Override
	public boolean onTouchEvent(MotionEvent event) {

		if (event.getAction() == MotionEvent.ACTION_DOWN) {
			Toast.makeText(getContext(), "INGEDRUKT!", Toast.LENGTH_SHORT)
					.show();
		}

		if (event.getAction() == MotionEvent.ACTION_MOVE) {
			Log.d("CoppaView", "It's a drag");
		}

		if (event.getAction() == MotionEvent.ACTION_UP) {
			Toast.makeText(getContext(), "LOSGELATEN!", Toast.LENGTH_SHORT)
					.show();

			if (event.getY() > height - 100) {
				// Voorbeeld : Als de touch wordt losgelaten in de onderste 100
				// pixels van het scherm kunnen we hier code uitvoeren
			}
		}
		return true;
	}

	// Als de geassocieerde MainThread een
	// this.coppaView.onDraw(MainThread.canvas); doet, Wordt onderstaande code
	// uitgevoerd
	@Override
	protected void onDraw(Canvas canvas) {
		canvas.drawColor(Color.BLACK);
		// Onderstaande regel voert dus de draw routine uit van de
		// backGndManager class
		backGnd.draw(canvas);
	}
}

Dan BackGndManager.java ;

package happyworx.nl.coppa;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;

public class BackGndManager {

	private Bitmap bitmap;
	private Context context;
	public int width;
	public int height;
	public int x;
	public int y;
	public static Rect RectBackGnd = new Rect(0, 0, 0, 0);

	public BackGndManager(Bitmap bitmap, int x, int y) {

		this.bitmap = bitmap;
		width = bitmap.getWidth();
		height = bitmap.getHeight();
		this.x = x;
		this.y = y;
		this.RectBackGnd.set(0, 0, width, height);

	}

	public Bitmap getBitmap() {
		return bitmap;
	}

	public void setBitmap(Bitmap bitmap) {
		this.bitmap = bitmap;
	}

	public int getWidth() {
		return bitmap.getWidth();
	}

	public int getHeight() {
		return bitmap.getHeight();
	}

	public int getX() {
		return x;
	}

	public void setX(int x) {
		this.x = x;
	}

	public int getY() {
		return y;
	}

	public void setY(int y) {
		this.y = y;
	}

	public void draw(Canvas canvas) {
		// Hier zie je hoe we de CoppaView RectScreen gebruiken om de bitmap
		// zodanig te vervormen, dat hij het hele scherm vult.
		canvas.drawBitmap(bitmap, RectBackGnd, CoppaView.RectScreen, null);
	}

}

Dan, het kloppend hart van de Game-Loop; de MainThread.
package happyworx.nl.coppa;

import android.graphics.Canvas;
import android.util.Log;
import android.view.SurfaceHolder;

public class MainThread extends Thread {

	// Maak 2 variabelen voor de surfaceHolder en de View, die we later kunnen
	// gebruiken
	private SurfaceHolder surfaceHolder;
	private CoppaView coppaView;

	// Een aantal booleans geeft aan wat de status van de thread is
	public static boolean running;
	public static boolean paused = false;
	// Het canvas geeft een mogelijkheid om bitmaps te tekenen op het surface
	public static Canvas canvas;

	// Deze code wordt uitgevoerd op het moment dat de MainThread gecreerd wordt
	// (in de CoppaView, door de regel thread = new MainThread(getHolder(),
	// this);)

	public MainThread(SurfaceHolder surfaceHolder, CoppaView cview) {
		super();
		this.surfaceHolder = surfaceHolder;
		this.coppaView = cview;
	}

	// Met deze routine kunnen we de Thread vanuit een andere class stopzetten.
	public void setRunning(boolean running) {
		MainThread.running = running;
	}

	@Override
	public void run() {
		// Zo lang deze thread draait (running), moet de game worden geupdate en
		// getekend. Zie ook de grafische weergave van de game-architectuur
		// op http://happyworx.nl/blog
		while (running) {
			// Hier een stukje code dat we kunnen aanroepen door de boolean
			// paused op true te zetten
			// (bijvoorbeeld met een pauzeknop elders in de game)
			while (paused && running) {
				try {
					sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}

			MainThread.canvas = null;
			// try locking the canvas for exclusive pixel editing on the surface

			try {
				MainThread.canvas = this.surfaceHolder.lockCanvas();
				synchronized (surfaceHolder) {

					// Deze heb ik even ge-comment. In de routine
					// coppaView.update() zou je allerlei bewerkingen kunnen
					// doen die noodzakelijk zijn voor het updaten van de
					// objecten in de game. (is er iets opgegeten / afgeschoten
					// / verborgen / gebotst etc.)
					// this.coppaView.update();

					// Teken de view op het scherm (dit statement start dus de
					// routine onDraw in de View waar deze instantie van de
					// Thread bij hoort.

					this.coppaView.onDraw(MainThread.canvas);
				}

			} finally {
				if (MainThread.canvas != null) {
					surfaceHolder.unlockCanvasAndPost(MainThread.canvas);
				}
			}
		}
	}
}

Denk aan je AndroidManifest.XML. in onderstaande voorbeeld zit een intent-filter. Ik weet niet zeker of die hier op z’n plek is… Een intent-filter wordt gebruikt door het systeem om aan te geven op welke manier de applicatie om moet gaan met code die verzoekt ‘iets’ uit te voeren. (bijvoorbeeld een URL, of een share-actie). Omdat deze game-loop nog niets buiten de eigen code uitvoert, zou de intent-filter wegkunnen. Overigens : Als je advertenties gaat toevoegen in je game, moet je ook in je AndroidManifest aangeven dat je applicatie gebruik maakt van de permissies INTERNET en ACCESS_NETWORK_STATE… Daarover later vast wel meer.

AndroidManifest.xml :

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="happyworx.nl.coppa"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="10" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".CoppaActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Als je deze code snippets op de juiste manier in je project hebt geplakt (en de background image hebt gemaakt!) dan kun je proefdraaien. Als het goed is zie je dan een scherm, gevuld met je background image. Door te spelen met de onDraw-routines kun je proberen andere objecten toe te voegen (maak eens een class die erg lijkt op de BackGndManager, maar gebruik voor de bitmap een kleiner plaatje, bijvoorbeeld).

..daarna ga je aan de slag met de OnTouchEvent routines. Elk object (als je mijn voorbeeld classes gebruikt) heeft een getX, setX, getY en setY. Met die stukjes code kun je de plaatsing van het object beïnvloeden. Gebruik hiervoor de regel canvas.drawBitmap(bitmap, X – width/2, Y – height/2, null); in de draw routine van je object. De coördinaten die je opgeeft in die regel staan voor de LINKER BOVENHOEK van de bitmap! Daarom trek ik er de breedte en hoogte gedeeld door 2 vanaf. Zo heb ik het object met het centrum op de aangegeven plek geplaatst.

In een volgende how-to hoop ik uit te leggen hoe je deze simpele gameloop kunt uitbreiden met bijvoorbeeld een appel. In die tutorial kijken we direct naar het herkennen van een botsing tussen twee objecten. Je hebt dan de basis voor aPpLeZ  😀

You can leave a response, or trackback from your own site.

4 Responses to “How-to : Een simpele Game Loop”

  1. Is Is it ok if I copy your entry in part,I will reference back to your website?

  2. Android games schreef:

    I’m extremely impressed with your writing skills as well as with the layout for your weblog. Is that this a paid theme or did you modify it your self? Either way keep up the nice quality writing, it is uncommon to peer a great weblog like this one today..

    • admin schreef:

      Thanks!! Glad you like it. The theme is free, I modified some elements. As you probably know, the hardest part is to keep coming up with content. Especially when I’m not developing for a while. I think the winter will do this blog good 😉

      Again, thanks for your compliments and happy reading 🙂

Leave a Reply