This is the script used for a talk I gave at the Central Wisconsin Developers Group on 01/28/2015.
Background
Welcome. Almost every game implements a game loop.
Quick show of hands
- Who here has created a game?
- Who has published a game?
- Did anyone here create a game before 2000? After 2000?
A lot of things have changed since then. What was once the hardest part of writing a game, the graphics engine, is greatly simplified. Image formats have been standardized and lots of sample code is available for reading and writing these images. Simple graphic libraries know how to draw a line, circle or arch. But most importantly, they know how to draw a bitmap or image.
Let’s jump right into some code.
while (true) { // Do something }
Can anyone tell me what is wrong with this code?
If you said an infinite loop, then you’d be correct… if we weren’t writing a game. That, is in essence a game loop. Thanks for coming…
There’s actually a little more to a game loop than that. But it’s still pretty simple.
while (isRunning) { // Get input // Update position // Clear screen // Draw }
We’re just going to keep doing that until we quit. That’s it. That’s the basics of a game loop. Let’s go ahead and jump into some code.
Getting Started
Do we have any Android developers in the room? OK. When I get stuck, I’ll need you guys to help me.
Android Studio -> New Project Application name: gameloop Company Domain: heeresonline.com
Eclipse / Domain Precedence
Anyone here use Eclipse for Android development? Show of hands? Anyone notice a difference? For Java applications, it’s recommended to write in domain precedence (com.heeresonline.application).
- Use API 16
- Blank Activity
Gradle, which is the new Android build system, will configure our project for us.
- Remove the layout and the text.
- Add image icon. Set icon to ic_launcher.
- Make sure that icon is not at X=0 or Y=0.
- Remove the padding* attributes.
- Close XML layout.
Open the MainActivity.java class.
Remove everything but the onCreate() method.
For the sake or brevity, we’ll skip over a discussion of the Android lifecycle. But essentially we use the onCreate() as our constructor.
The first step is to grab a reference to our ImageView using the standard Android resource names.
final ImageView image = (ImageView) findViewById(R.id.imageView);
Next, we need some way to refresh the screen at a set interval. For our initial naive implementation, we’ll schedule a timer to execute every X milliseconds. Looking through the Android library, the Timer class should work for us.
The timer is basically a way to run threads on a Java platform.
Add the following to the the onCreate method.
private final int FPS = 40; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final ImageView image = (ImageView) findViewById(R.id.imageView); Timer timer = new Timer(); timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { } }, 0, 1000 / FPS); }
We create our timer and schedule it to run at a “fixed” rate. The first parameter is our delegate to run. The second parameter is the delay before the task is run. The third parameter is the period or how often the timer should run. So is case, we want 40 fps, so the timer will execute every 25 milliseconds.
1000 milliseconds = 1 second / 40 frames per seconds = 25 milliseconds per frame
Inside of our run() is where our game loop will live.
Before we get too far, we need to create a couple of variables to keep track of our game sprite.
private int directionX = 1; private int directionY = 1; private int speed = 10;
The direction parameters we’ll use to easily reverse our sprite direction. The speed is obviously how many pixels the sprite moves per “tick” of our game loop. So, lets update our game loop to move our sprite.
First we’ll get the current position.
int x = image.getX(); int y = image.getY();
Then we’ll go ahead and increment that position.
image.setX(x + (speed * directionX)); image.setY(y + (speed * directionY));
Threads Beware
But, can anyone tell me what is wrong with this code?
Correct, only the UI thread can access UI components. To do that, we have to request our code to run on the UI thread. If the current thread is NOT the UI thread, then an event is posted to the UI thread queue.
MainActivity.this.runOnUiThread(new Runnable() { @Override public void run() { } });
And then we’ll move our code into the runOnUiThread method.
Let’s go ahead and see what we’ve created so far. Oops… our sprite went off the screen and is going off to explore pluto…
So what we need to do now is some basic collision detection. We want to change the X or Y direction if the sprite hits one of the edges. First thing we need to do, is get the bounds for our screen. To do that, we’ll use the WindowManager class to get a Display object:
private Point screen = new Point(); @Override protected void onCreate(Bundle savedInstanceState) { Display display = getWindowManager().getDefaultDisplay(); display.getSize(screen);
Now we know our left and bottom limits. Let’s update our movement calculations:
int x = (int) image.getX(); int y = (int) image.getY(); if ((x >= screen.x) || (x <= 0)) { directionX *= -1; } if ((y >= screen.y) || (y <= 0)) { directionY *= -1; } image.setX(x + (speed * directionX)); image.setY(y + (speed * directionY));
If you watch closely, you can see that our sprite moves behind the android title bar and virtual buttons at the bottom of the screen. We have a couple of options to solve this depending on if you want the title bar and buttons to be available. Since we’re creating a game, we probably want a full screen view anyway so we’ll just go ahead and turn the title off. Add the following to the onCreate() method.
requestWindowFeature(Window.FEATURE_NO_TITLE); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
Optionally, the same can be accomplished by changing the default theme. This may be set in the AndroidManifest.xml file or the style.xml file. The setting we want to use is:
Theme.AppCompat.Light.NoActionBar
The other problem we see is that our spite goes too far to the right or bottom. The reason being is that we’re ignoring the width and height of the item. If the Android canvas system, the X,Y coordinate refers to the upper left corner. To fix this, we just need to take the width and height into consideration for the the right and bottom.
if (((x + (image.getWidth())) >= screen.x) || (x <= 0)) { directionX *= -1; } if (((y + image.getHeight()) >= screen.y) || (y <= 0)) { directionY *= -1; }
If you want the item to move faster, we can go ahead and increase our speeds.
private int speed = 100;
And there we have a simple game loop.
So what do you think?
What’s wrong with our implementation? There are a number of problems, a few of which are:
- It’s impractical to pre-create our game scene. A game will typically have hundreds of items on the screen at a time.
- Our implementation is NOT very efficient.
- We don’t handle lag or slower devices.
Using the SurfaceView
So, lets work on fixing some of those problems. Android actually provides a view called SurfaceView which gives us more control over our rendering. Start off by creating a new java class inherited from SurfaceView.
public class GameSurfaceView extends SurfaceView { public GameSurfaceView(Context context) { super(context); } }
Now, lets update our MainActivity to use our new SurfaceView. We’ll remove the majority of the code that we wrote. The main thing is that we’ll set the content view to be our new GameSurfaceView which we’ll create.
protected GameSurfaceView gameView; public class MainActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { requestWindowFeature(Window.FEATURE_NO_TITLE); super.onCreate(savedInstanceState); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); gameView = new GameSurfaceView(this); setContentView(gameView); } }
We’ll also want to handle some of the Android activity lifecycle. In particular, we want our game loop to pause and start based on the activity lifecycle. Our users wouldn’t like the game to keep running in the background. We’ll need to override the onPause() and onResume() methods. Add the following code:
@Override protected void onResume() { super.onResume(); gameView.resume(); } @Override protected void onPause() { super.onPause(); gameView.pause(); }
The pause() and resume() methods are displaying an error because we need to write them quickly. We’ll stub them out in our GameSurfaceView class.
/** * Start or resume the game. */ public void resume() { } /** * Pause the game loop */ public void pause() { }
With any development, not only Android, we don’t want any CPU intensive operations to run on our UI thread. So we’ll setup our SurfaceView implementation to implement threading.
public class GameSurfaceView extends SurfaceView implements Runnable @Override public void run() { }
Let’s finish handling our thread. First let’s define some variable. The first is a boolean to know if the thread should be running, the second is our thread variable.
private boolean isRunning = false; private Thread gameThread; public void resume() { isRunning = true; gameThread = new Thread(this); gameThread.start(); }
The pause() is a little more complicated due to handling the thread termination and potential exception. First we change our isRunning flag to stop the game loop. Then we wait for our game thread to exit.
/** * Pause the game loop */ public void pause() { isRunning = false; boolean retry = true; while (retry) { try { gameThread.join(); retry = false; } catch (InterruptedException e) { // try again shutting down the thread } } }
Now, all that remains is to handle the run() portion of the thread. But before we do that, we need to look at how the SurfaceView works. The SurfaceView is designed so it is Z ordered behind the window holding it. The SurfaceView then punches a hole in it’s window to allow its surface to be displayed. The standard view hierarchy will take care of additional view compositioning. In short, this allows other views (i.e. buttons, labels, etc.) to overlay the surface.
With the SurfaceView, we first need to lock the canvas to request a frame. Then we render the frame. And lastly we unlock the frame and notify the Android system to update. The SurfaceHolder class helps to manage this for us.
private SurfaceHolder holder; public GameSurfaceView(Context context) { super(context); holder = getHolder(); // Here we can handle additional surface notifications (i.e. created, destroyed, etc.) // holder.addCallback(); }
Next, we fill in the outline for our run() method.
@Override public void run() { while(isRunning) { // We need to make sure that the surface is ready if (! holder.getSurface().isValid()) { continue; } // update // draw Canvas canvas = holder.lockCanvas(); if (canvas != null) { // canvas.draw(...); holder.unlockCanvasAndPost(canvas); } } }
Based on the Android documentation, the Surface may NOT always be available. So at each iteration of our loop, we first check to see if the surface is ready. Then we grab our canvas by requesting a lock, draw to the canvas and then release the lock on that frame.
But before we get too far, we need to add a few variables to track time. Why? Our current loop doesn’t take into account CPU speed. So on older devices, our game will run slow but on newer devices it will run too fast.
*** Switch to presentation ***
Add the following constants which will determine our update interval.
private final static int MAX_FPS = 40; //desired fps private final static int FRAME_PERIOD = 1000 / MAX_FPS; // the frame period
Now, we need to keep track of how much time has elapsed since the last update. To do that, we’ll just keep updating a variable which we can use to calculate the delta or difference in time.
public void run() { while(isRunning) { // We need to make sure that the surface is ready if (! holder.getSurface().isValid()) { continue; } long started = System.currentTimeMillis(); // update // draw Canvas canvas = holder.lockCanvas(); if (canvas != null) { // canvas.draw(...); holder.unlockCanvasAndPost(canvas); } float deltaTime = (System.currentTimeMillis() - started); } }
Now, we’ll handle the case where our loop game loop finishes too soon. After the draw, we add the following:
int sleepTime = (int) (FRAME_PERIOD - deltaTime); if (sleepTime > 0) { try { gameThread.sleep(sleepTime); } catch (InterruptedException e) { } }
If the time it took us to update or game loop and render is less than the expected FRAME_PERIOD, then we simply sleep the thread the specified amount of time.
However, in the case that it took us too long to execute, we instead will want to skip the rendering code to get back in sync. We’ll simply keep updating our game position until we’re back into sync.
while (sleepTime < 0) { // update sleepTime += FRAME_PERIOD; }
In your code, you’d probably want to keep track of the number of skipped frames and only allow a certain number of frames to skip.
Now we need to actually fill in our update loop. Since we need to call that code from two different places, it’s obvious we should create a method. Let’s create our update() method.
protected void step() { } @Override public void run() { ... // update step(); ... while (sleepTime < 0) { step(); sleepTime += FRAME_PERIOD; } ... }
OK. So lets get something drawn on the screen. To start, lets create a Sprite object to encapsulate our game object. This will allow us to manage multiple objects.
class Sprite { int x; int y; int directionX = 1; int directionY = 1; int speed = 100; Bitmap image; public Sprite(int x, int y) { this.x = x; this.y = y; } public Sprite(int x, int y, Bitmap image) { this(x, y); this.image = image; } }
Now, lets draw it to the canvas and lets take the opportunity to refactor our draw method.
protected void render(Canvas canvas) { } @Override public void run() { ... // draw Canvas canvas = holder.lockCanvas(); if (canvas != null) { render(canvas); holder.unlockCanvasAndPost(canvas); } ... }
In our draw, we’ll create our Sprite object and draw it to the screen.
private int x = 100; private int y = 100; protected void render(Canvas canvas) { Sprite sprite = new Sprite(x, y); sprite.image = BitmapFactory.decodeResource(this.getResources(), R.drawable.ic_launcher); canvas.drawBitmap(sprite.image, sprite.x, sprite.y, null); }
Let’s go ahead and see try this.
Now what’s wrong…
So can anyone tell me what’s wrong here?
If you said that the Sprite and bitmap load shouldn’t be happening in the render() method then you’re correct. Loading bitmaps should be done during our game or level initialization. Both our update() and render() methods need to be as lean as possible.
So, let’s go ahead and fix that by created an array of elements.
private Sprite[] sprites; public GameSurfaceView(Context context) { ... sprites = new Sprite[] { new Sprite(100, 100, BitmapFactory.decodeResource(this.getResources(), R.drawable.ic_launcher)) } } protected void render(Canvas canvas) { for (int index = 0, length = sprites.length; index < length; index++) { canvas.drawBitmap(sprites[index].image, sprites[index].x, sprites[index].y, null); } }
Now we can go ahead and add our game logic to move our sprite to the step() method.
protected void step() { for (int index = 0, length = sprites.length; index < length; index++) { Sprite sprite = sprites[index]; if ((sprite.x < 0) || ((sprite.x + sprite.image.getWidth()) > screenWidth)) { sprite.directionX *= -1; } if ((sprite.y < 0) || ((sprite.y + sprite.image.getHeight()) > screenHeight)) { sprite.directionY *= -1; } sprite.x += (sprite.directionX * sprite.speed); sprite.y += (sprite.directionY * sprite.speed); } }
We need to head back and set the screen width and height. We can use the callback that we ignored earlier and implement the surfaceChanged() method.
private int screenWidth; private int screenHeight; public GameSurfaceView(Context context) { super(context); holder = getHolder(); holder.addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { screenWidth = width; screenHeight = height; } @Override public void surfaceDestroyed(SurfaceHolder holder) { } }); ... }
So let’s run it. You’ll notice that we have a “ghosting” effect. The reason being is that when we implement a SurfaceView, we are responsible for invalidating or clearing the screen. The fix is simple. Add the following to our render() method.
protected void render(Canvas canvas) { canvas.drawColor(Color.BLACK); for (int index = 0, length = sprites.length; index < length; index++) { canvas.drawBitmap(sprites[index].image, sprites[index].x, sprites[index].y, null); } }
Of course, a game with only one item on the screen is boring. So let’s add a new item and get them to interact with each other. Add another new item.
sprites = new Sprite[] { new Sprite(100, 100, BitmapFactory.decodeResource(this.getResources(), R.drawable.ic_launcher)), new Sprite(600, 400, BitmapFactory.decodeResource(this.getResources(), R.drawable.ic_launcher)) };
Let’s make sure that works.
Let’s make it easier to tell the difference between our two sprites. We could use a new bitmap. But instead, let’s just add a color attribute to our sprite.
class Sprite { ... int color = 0; ... public Sprite(int x, int y, Bitmap image, int color) { this(x, y, image); this.color = color; } }
Now let’s update our render() method to draw with a color overlay.
protected void render(Canvas canvas) { canvas.drawColor(Color.BLACK); for (int index = 0, length = sprites.length; index < length; index++) { Paint p = null; if (sprites[index].color != 0) { p = new Paint(); ColorFilter filter = new LightingColorFilter(sprites[index].color, 0); p.setColorFilter(filter); } canvas.drawBitmap(sprites[index].image, sprites[index].x, sprites[index].y, p); } }
Lastly, let’s update our initializer to set the color.
sprites = new Sprite[] { new Sprite(100, 100, BitmapFactory.decodeResource(this.getResources(), R.drawable.ic_launcher)), new Sprite(600, 400, BitmapFactory.decodeResource(this.getResources(), R.drawable.ic_launcher), Color.RED) };
Now, lets add some collision detection. Android provides a Rect class that provides a static intersects() method. We’ll add the following collision detection code into our step() method.
if ((sprite.y < 0) || ((sprite.y + sprite.image.getHeight()) > screenHeight)) { sprite.directionY *= -1; } Rect current = new Rect(sprite.x, sprite.y, sprite.x + sprite.image.getWidth(), sprite.y + sprite.image.getHeight()); for (int subindex = 0; subindex < length; subindex++) { if (subindex != index) { Sprite subsprite = sprites[subindex]; Rect other = new Rect(subsprite.x, subsprite.y, subsprite.x + subsprite.image.getWidth(), subsprite.y + subsprite.image.getHeight()); if (Rect.intersects(current, other)) { // Poor physics implementation. sprite.directionX *= -1; sprite.directionY *= -1; } } } sprite.x += (sprite.directionX * sprite.speed); sprite.y += (sprite.directionY * sprite.speed);
We can add more chaos by adding another Sprite.
sprites = new Sprite[] { new Sprite(100, 100, BitmapFactory.decodeResource(this.getResources(), R.drawable.ic_launcher)), new Sprite(600, 400, BitmapFactory.decodeResource(this.getResources(), R.drawable.ic_launcher), Color.RED) new Sprite(400, 800, BitmapFactory.decodeResource(this.getResources(), R.drawable.ic_launcher), Color.BLUE); };
That is the basics of a game loop and simple collision detection.
Clean Code != Good Game Code
When reviewing the code though, we’ve actually created some problems in our critical step() and render() methods. Essentially we’ve written “good” object oriented code, however when doing game development sometimes you need to do things “wrong”.
In any automatic “garbage collection” system, you want to avoid creating and destroying short lived objects. Why? Because our game loop time is so precious, we don’t want the garbage collector (GC) to unnecessarily take away time in our update cycle. To get around the problem, you’ll want to create a pool of reusable items that you recycle items.
If you’ve done any Android development and used a custom Adapter for lists, this is the same concept of recycling used objects.
SOURCE CODE (Github)
UNTIL NEXT TIME…