Game rendering
Download the source of this article.
Introduction
This article is about making a game in Android; more specifically, this article focusses on the screen updates.
Most games have a continuous loop ("main loop") doing two things: update the state of the game, and updating the view of the game. Assuming a tank battle game, the state is about where my tank is, where the bad guys tank is, and where his bullet is. An update of the state means recomputing the position of the bullet (given its speed), and maybe recompute my tanks position when I'm pressing the throttle key. Updating the view means (re)drawing the tanks and the bullets on the screen at the new location computed during the update of the state.
while( ! gameover ) { UpdateGameState(); UpdateGameView(); }
The above loop is a very simplistic main loop for a game. There are several articles, explaining beter techniques for a game loop. This has to do with getting the most UpdateGameView's per second (normally called frames-per-second or FPS), still having smooth movement, also when the load goes up (heftier state updates), and also on older (slower) computers. This artcile will not deal with game loops (see e.g. Glenn Fiedler's pages instead). It will focus on how to get the view updated.
I have identified two approaches for updating the view: calling invalidate or using the SurfaceView. Both methods need a timing mechanism. There are several choices for that too: a plain Android Timer, using Handler with delayed post. I use the latter, but wrapped in a class and called STimer. See the article on timers.
1 Timer
We start with developing a class that encompasses all logic for drawing the game, and only next focus on how to force an actual drawing. The class derives from View, let's call it GameView.
1.1 GameView class
In order to keep this article focused on the how part, we keep the drawing simple. Our GameView will draw the sun and the earth. It has one simple parameter alpha, that determines where the earth should be drawn in its orbit around the sun.
class GameView extends View { // remark 1 public double alpha= 0.0f; // remark 2 protected Paint mPaintSun= new Paint(); { // remark 3 mPaintSun.setAntiAlias(true); mPaintSun.setStyle(Paint.Style.FILL); mPaintSun.setColor(0xFFFFFF00); } protected Paint mPaintEarth= new Paint(); { // remark 4 mPaintEarth.setAntiAlias(true); mPaintEarth.setStyle(Paint.Style.FILL); mPaintEarth.setColor(0xFFCCBB99); } public GameView(Context context) { // remark 5 super(context); } @Override public void onDraw(Canvas canvas) { // remark 6 double x_sun = getWidth()/2; double y_sun = getHeight()/2; double radius_sun= Math.min(x_sun,y_sun)/6; double radius_earth= radius_sun/6; double radius_orbit= Math.min(x_sun,y_sun) - radius_earth*1.1; canvas.drawCircle( (float)x_sun, (float)y_sun, (float)radius_sun, mPaintSun ); double x= x_sun + radius_orbit * Math.cos(alpha); double y= y_sun + radius_orbit * Math.sin(alpha); canvas.drawCircle( (float)x, (float)y, (float)radius_earth, mPaintEarth ); } };
There are several remarks about this fragment, but none has to do with how to draw. We'll make then nevertheless.
Remark 1 The GameView derives from View. This means it is just like a button or a text view; it could be listed in the layout xml, or it could be created dynamically. It also means that one has the responsability to override some functions, most notably onDraw and onMeasure. To keep it simple, we stay with the default implementation of onMeasure (the base class implementation defaults to the complete background size), and only implement onDraw.
Remark 2 The alpha field determines where the earth should be drawn in its orbit (see onDraw). Normally, there would be a public setAlpha method, setting this field. More importantly, this method would also call invalidate, which would cause the onDraw being called. We don't want that in our game. The state update function will typically set a lot of member fields in GameView, and only call invalidate once, explicitly.
Remark 3 To draw the sun, we need a so-called Paint. A paint holds style (stroke, fill), color (yellow, green), transparancy, antialias and many more settings when drawing (a line, letter, bitmap). We want a yellow, filled sun with anti-alias.
Note the very peculiar way I've written the code initializing mPaintSun. The beginning (protected Paint mPaintSun= new Paint();) is straight forward; a protected field mPaintSun of type Paint is declared and created in the initializer, using new Paint(). That ends the field declaration; note the semi colon. Next comes an opening brace. It uses suggestive formatting, but this is actually a standalone piece of code known as an initializer block. The Java compiler copies initializer blocks into every constructor of the class.
Remark 4 The next fragment declares, creates an initializes (again via an initializer block) the paint for the earth (mPaintEarth).
Remark 5 We are lazy here. We will only create an instance of GameView programatically (and not via XML), so we have implemented just the constructor needed for that.
Remark 6 The onDraw(Canvas canvas) method override the default (which is an empty method, drawing nothing). Its purpose is to draw draw the sun and the earth, the earth at a position alpha, on the passed canvas.
The code starts by computing some key parameters. The first two lines, get the size of the drawing area (getWidth and getHeight), and set the sun in the centre. The next line checks where is the least room horizontally or vertically (typically horizontally when the device is in portrait mode and vertically in landscape), and divide that by six to form the radius for the sun. The next line sets the radius of the earth to 1/6th of that of the sun. The final parameter is the radius of the earth orbit; it is set to such a value that the earth does not hit the border of the screen, leaving only 0.1 times the earth radius as margin.
Then, the sun is drawn, at position (x_sun, y_sun), using the radius and paint for the sun. Finally, we compute the coordinates for the earth using alpha, and draw the earth.
1.2 Activity
We have created an activity GameTimerActivity. The constructor loads the xml file (containing a linear layout), looks up the linear layout, and adds the game view which is created programatically (red).
public class GameTimerActivity extends Activity { LinearLayout mLayout; GameView mGameview; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Add GameView programmatically mLayout= (LinearLayout)findViewById(R.id.layout); mGameview= new GameView(this); mLayout.addView(mGameview); } }
Next, we add two helper methods to the activity class, the two update functions mentioned in the introduction. Observe that the UpdateGameState changes the alpha (in this case increments it, so that the earth will make nice orbits). There is no call to invalidate, so the screen will not be redrawn. The redraw is initiated by UpdateGameView, which invalidates the mGameview.
protected void UpdateGameState() { mGameview.alpha += 2*Math.PI/100; } protected void UpdateGameView() { mGameview.invalidate(); }
1.3 Time
For the time base, we use the STimer as developed in a previous article on timers.
public class GameRenderActivity extends Activity { STimer mSTimer; ... @Override public void onCreate(Bundle savedInstanceState) { ... // Init timer mSTimer= new STimer(); mSTimer.setPeriod(50); // Alarm causes update mSTimer.setOnAlarmListener( new OnAlarmListener() { @Override public void OnAlarm(STimer source) { UpdateGameState(); UpdateGameView(); } }); } }
By setting the alarm listener to a function that calls the two update functions, we achieve the periodic update of the game (state and view); see the red part.
1.4 Game logic
In this last section, we add a bell and a whistle.
First of all, we allow the user to touch the screen, making the earth stop orbitting (or continue when it was stopped). This is simply achieved by adding an on click handler to the GameView (red part).
@Override public void onCreate(Bundle savedInstanceState) { ... mGameview.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { mSTimer.setEnabled( ! mSTimer.getEnabled() ); } }); }
Secondly, we want the game to end after two full rounds of the earth. For this, we modify the update function (red line added), which cancels the timer when alpha exceeds 2 times 2pi.
protected void UpdateGameState() { mGameview.alpha += 2*Math.PI/100; if( mGameview.alpha>2*2*Math.PI ) mSTimer.setEnabled(false); }
2 SurfaceView
Another option is SurfaceView. It is used in LunarLander and JetBoy that comes as an example in the Android SDK. I have not yet investigated that. More real-time.
Download the source of this article.