In 2003 I wrote a simple casual game for a computer graphics course as part of my masters' degree program. While the game itself was nothing special -- a simple 2-dimensional app where balls bounced around the screen and the player had to click on each of them in ascending order before a timer expired -- I thought it would be worth revisiting since it would provide a way to learn the way in which to handle graphics and animation on the Android platform.
Since the initial implementation of the game was an assignment to introduce the concept of double-buffering and simple animation, not much attention was paid to organizing the game logic in a manner that made it easy to add new game modes and rules. To facilitate this, the code was completely reorganized into a few discrete components. In the newly refactored game, the overall architecture is still pretty simple. It consists of a model class that implements all the game rules, score keeping, and the current state of the game, a custom data-structure to maintain the state of individual "balls", an animation thread that determines if and when to draw the objects and a view class that services as the bridge between the Android system (and things like touch events, menu key presses, etc) and the game. In the sections that follow, each of the components will be outlined with special emphasis on anything that was done differently due to capabilities/limitations of the Android platform.
Model & Data Structures:The model class was responsible for maintaining the current state of the game. This state includes the score, the current level, the amount of time remaining, the list of all the Ball objects which, in turn, contain their position, that will be displayed and the current game mode (running, paused, game-over, etc). For simplicity of maintaining game state upon activity pause, all members of the model class must be serializable since they will be serialized into and out of a Bundle object when the game's Activity is suspended or resumed.
In addition to holding the state, the model class needed to contain the game logic. This consists of two main functions: determining if a particular set of coordinates are within the "target" ball and determining if two balls have collided. In the original implementation, this was done via an inelegant, brute-force manner that was expensive in terms of both memory and CPU cycles. Since the Android platform is more resource-constrained than your average desktop, reducing the processing required for each iteration through the program's main loop was necessary.
Collision Detection:In the old version of the game, a two-dimensional array of bits the same size as the screen was allocated and the bits were set to either 0 or 1 based on whether or not some ball occupied that pixel. Every time the ball moved, the bits at its origin location were cleared and the bits at the destination were checked. If they were already set, a collision occurred and the ball changed direction and speed (i.e. it bounced) but if the bits were clear, then the space was empty so the ball's position was updated and the bits were set.
To reduce the need to iterate over every pixel on the screen, the new implementation segments the playing surface into a grid where each cell is approximately 100x100 pixels. An ArrayList is allocated for each cell in the grid and balls are to the list that corresponds to the cell in which their center lies. Since balls are always smaller than the cells, a ball in cell i,j can only collide with a ball that is in the 9-cell "superblock" surrounding it (cell i,j and the up-to 8 cells that surround it). To check for collisions, the list of all balls in the superblock is retrieved and the Euclidian distance is calculated between the ball being checked and every other ball in the list. If the distance measure is less than the sum of the balls radii, then the balls are overlapping and thus have collided.
Animation:The actual logic for updating the state of all the model components (i.e. moving all the balls) remained fairly unchanged from the original implementation. What was added, however, was logic to attempt to keep the frame rate constant regardless of the CPU clock speed on the target device (something that, in retrospect, should have been in the original implementation too). To keep the animation looking smooth, a frame rate of 25 fps was desired. This equates to 40 milliseconds per frame. The code in the animation loop tracks how long it takes to do the state update and the drawing and if that time was less than 40 ms, it will sleep for the remaining time. This approach is about as simple a mechanism as can be employed and, obviously, it is totally ineffectual if the actual time needed to render the frame exceeds 40 milliseconds. Luckily, for this game, that time is sufficient on all device configurations tested.
Communication between the animation thread and the UI thread was another item that needed to be handled in an Android-specific manner. Some events in the game were not triggered by user interaction but rather by a condition within the model (the timer expiration, for instance). When the timer expired, a modal dialog box showing the final score was to be displayed. Rather than drawing one on the surface manually, it was much easier to use the built in DialogBuilder to pop-up an AlertDialog. If this was done directly in the model class as part of the updateState method (the method called by the animation loop to update the ball positions, timer and other state information) it would throw an exception since this was not running on the UI thread. Instead, a
Handler had to be used to pass a message from the animation thread back up to the UI thread. The message could, in turn, contain a custom GameEvent class that allowed the UI thread to react appropriately.
View:To adapt the view classes, the first thing that had to be done to port the game was to re-plumb all the drawing logic. Since the original implementation was created using the Java AWT and drawn directly to the ContentPanel of an Applet using the methods of
java.awt.Graphics all the code that actually wrote the pixels to the screen needed to be revisited.
Android provides a Canvas upon which arbitrary shapes and text can be drawn. If you want to add the canvas to a view that may or may not contain other views, a
SurfaceView is a good choice since it provides a drawing surface embedded in the normal view hierarchy. As the developer, you can control the contents and size of the surface but the system takes care of positioning it at the right place on the screen (based on whatever else is in your view hierarchy).
Drawing on an Android Canvas was not that different from drawing on a JPanel. The difference, however, was in the view life cycle. Just like any other Android application, the system can suspend your Activity any time the user does something that causes app to lose focus (like taking a call, for instance). At that point, the surface on which you're drawing may be destroyed (but not always).
In response to the Activity pause event, the game pauses the animation (by setting a volatile flag on model class) which tells the animation routine to stop updating the position of the balls and to stop decrementing the timer. At this point, the animation thread runs to completion (to be a good citizen and avoid busy-waiting in the background). When the user returns to the game, the game animation can be resumed. What we cannot do, however, is resume the thread (since you cannot call Thread.start() on the same thread more than once). Instead, the handler must create a new thread and seed it with the information from the "old" game model (instead of creating a new model instance) and start the thread.
Understanding the life cycle of the view classes is important. It is worth pointing out that it is not handled correctly in the
Lunar Lander sample application included on the Android development site. In that example, the surfaceCreated method on the view class starts the thread that was initialized in the constructor. The problem is that surfaceCreated can be called by the system whenever the surface regains the foreground (even though the View's constructor may not have been called since it is the same view instance). Since the thread is a member variable and may have already been started, calling start could (and often does) result in an exception. This isn't meant to be a critique of the example code (I've said it before and I'll say it again: the wealth of examples and documentation is one of the strengths of the Android platform) but it should be noted that not everything in the example code is 100% correct all the time and if something "feels" wrong, it very well may be.
Impressions and Future Work:Porting this application was a worthwhile exercise. Since the "game" portion was already done, it allowed me to focus on the nuances of the platform itself rather than the mechanics of the game play. All said and done, it took approximately 15 hours from start to finish (it could have been a lot shorted had I kept the brute-force collision detection instead of refactoring that). It was time well-spent in that it helped illustrate some of the finer points of handling the animation (frame rate limiting, surface life cycle, percolation of events back up to the UI thread).
Despite the fact that game itself is exceedingly simple, I plan on adding to it in the coming weeks. I am contemplating using the game as a proof-of-concept for a server-side component that can handle reporting high scores, issuing user challenges and handling teams/clans. I plan on exposing the server platform via a REST API and building a client library that could be included in any Java application for easy integration. I'd be interested in hearing if any of the (few) people who read this think there is any merit in a system like that and, if so, what other features it should support. I hope to have more to say on this topic soon.