Saturday, 26 January 2013

BattleShots Game Architecture


The implementation of BattleShots is a little more complex than the standard android practice of creating an XML based layout and attaching events to components. To achieve a customised UI which can match a theme we have control of, we need to take charge of all drawing. This in turn severely limits the use of built in widgets for the android framework as they generally are themed with the OS version or skin in mind. Whilst they do provide theming support such as color, border, background etc. we still cannot achieve the majority of styles we want to expose from our game UI mockups.
In steps OpenGL which is the most heavily used 2d/3d graphics API at this time. Android contains a flavour of OpenGL in the form of OpenGL ES 1.0 and OpenGL ES 2.0 (ES stands for Embedded-Systems) which is optimized for mobiles, pda's and game consoles etc.
From android version 1.0 OpenGL ES 1.0 has been available. As of version 2.2 (froyo) OpenGL ES 2.0 has been available. Both make use of hardware acceleration where devices have separate GPU's which make the API very quick allowing games to achieve 60 FPS rendering (the more frames the better).
Seeing as we wanted BattleShots to be a rich UI experience we needed to go this route or our UI would be very static.

Game Architecture

Down to the mechanics of it! It is generally bad practice in any coding circle to perform application logic in the UI thread (unless it's short lived). For example you dont want to start processing a report in the UI thread as you may want the user to see a loading screen with an update on percentage completed. In this instance you would spawn a thread, update the UI every n seconds and wait for completion.
The BattleShots game architecture follows a similar approach. When the android Activity is initially created we create two objects.
  • The Updater
  • The Renderer
The Updater handles all game logic such as checking if the user pressed on a square, rotating the target reticle, animating a message on/off screen.
The Renderer handles drawing all objects to the screen.
We don't want to merge the above into one big routine as the frame rate will drop severely for devices that take a long time in the updating part.

Activity State

An android Activity has several states. See Android Activity Lifecycle for all its states (there is a good diagram on that page). The ones we are interested in are:
  • onCreate
  • onPause
  • onResume
onCreate we create a new OpenGL based view GLSurfaceView which is similar to a normal xml based view except that it expects us to render everything. Secondly we create The Renderer instance referred to above. Finally we create The Updater referred to above. Think of this event as the user starting a new game so we have to regenerate all our rendering and game logic.
onPause when the user opens a new app, goes to the home screen, receives an incoming call etc. We hijack this event to stop our updater thread from running, this conserves battery life (as we don't have a thread spinning away) and it also means the game doesnt continue updating, effectively pausing it. Basically any time our UI goes away.
onResume when the user returns to our app and it is still in memory (not been garbage collected). We hijack this event to resume our updater thread, this retains the state as we are not re-creating the objects over and over again. Basically any time our UI comes back after using it previously.

The Updater (Update Loop)

The updater (referred to in the game dev world as Update Loop or Game Loop) handles all game logic as previously stated. It has two purposes.
  • Provide a list of drawing commands to the renderer
  • Notify the UI thread of events e.g. user fired at a square
The class com.pterodactyl.battleshots.views.BoardGameUpdater is where you want to look for its implementation. It implements the Runnable interface which means it is intended to be run by a thread at some point. What we actually do in the classes run() method is force an infinite loop when the thread starts so the updater continually runs. The loop can be broken by setting the running flag to false.
Within the loop there is code to handle the number of updates per second. We don't need the updater to run at 1,000,000 times a second and the more we can sleep the thread the more battery life and cpu we save. Currently the updater thread runs at a maximum of 30 updates per second as defined by the MAX_UPDATES_PER_SECOND variable in its class. The logic within the loop handles the three following instances.
  • Hardware is fast - sleeps the thread after every loop to meet the 30 updates per second rate)
  • Hardware is just right - continually loops with no sleeping
  • Hardware is slow - it forces several updates without sleeping and without pushing updates onto the render thread as it tries to play catchup
Within a single update loop the class does four things:
  • Updates Game Objects
  • Triggers Events
  • Create Drawing Commands
  • Send Drawing Commands To Renderer
Update Game Objects the game state is updated every cycle. It creates objects for each UI instance e.g. a boat, a square, a user icon, text etc. and updates their state. Some common updates are:
  • Update the target reticle rotation value (so it spins)
  • Changes the easing value of a text background (so it animates up and down)
  • Calculates the size and position of the checkers based on screen width/height
  • Calculates the size and position text
  • Calculates the size and position of splashes and explosions
Triggers Events calls any event listeners for events in the game such as a user pressing the fire button.
Create Drawing Commands creates a single drawing command (which a class that wraps up OpenGL drawing commands). It creates objects for each UI instance and uses their state from above to position, draw, colourize etc. Some common drawing commands are:
  • Load a splash texture, position it on screen and draw it
  • Load a background texture, repeat it across the entire background and blend it with a blue gradient
  • Draw a checker using native quads and colour them in
Send Drawing Commands To Renderer passes the above commands to the renderer. Whilst this sounds easy enough it is one of the main reason's why the renderer and updater are broken into separate components. Because updating and rendering are done independently on separate threads, for us to pass information from the updater to the renderer we need to do it safely by locking the renderer and updating its command list. Locking is quite a slow process so it is only done once at the end of an update cycle.

The Renderer (Render Loop)

The renderer is quite dumb. For each rendering cycle (an infinite loop controlled by OpenGL) it locks the Drawing Command list, iterates through and draws them to the screen, finally unlocking the list. This allows for the UI to be drawn very quickly as very little logic (apart from translating coordinates or scaling) is done on this thread. This is how we achieve a high FPS.

The Update Loop + Render Loop = Game On

I have put together a diagram below to try to explain the above process visually. Hope it helps.

No comments:

Post a Comment