Version 1.0 of Light Racer didn't have much to say for architecture. There was a single Activity which dealt with the buttons, showed instructions and displayed the main game View. I'll cover the way the menus are going to work in a different article but suffice to say for now that having a single Activity deal with multiple different things is not a good idea at all in Android. The main View was a SurfaceView and simply started up the main game thread. The Thread class was the main game and besides the Sound Manager and a very light Player class which held the state, path and animation information for any given player, there wasn't a whole lot to it. This made it easy to develop but the new requirements are to have NPCs (Non Player Characters), network multi-player, different 2D and 3D displays and more. How does one tackle such a problem? With a better design, of course.
The new design is much more organized and also more abstract. The following slides will show how I am changing the design to delegate far more responsibility into the Game Objects and Receivers and away from the main class. The main class's job will be simply to initialize the environment, load resources, initialize and configure the world, run the game and watch for changes to the Android environment.
The environment consists of the world (everything a game cares about and will display to the user), various managers like sound and notification managers, any object pools (because Android gets slow if you are creating and GCing lots of objects) and hooks into the Android environment.
Resources are things like graphics, sounds, fonts, and colors. Many parts of the game need to access the resource pools so it's always a good idea to have them managed by one object and pass that one object to anything requiring resources. For example, all GameObjects are responsible for drawing themselves so they will need to get their bitmaps from the resource manager. The main game class will have the resource manager release its resources when it's time to clean the game environment up.
The next diagram is a bit more concrete and recognizable. Player is the only Game Object on here but you can imagine that there might be others, such as a barrier or a power-up item. All of these things are Game Objects and are responsible for drawing and animating themselves. The Player will be doing collision detection on itself to check for a crash. If I end up adding power-ups, it will also check to see if it has touched or passed over an item. A Player maintains its own position in the world. It also maintains it's state so it knows if it's still alive or crashed. The path is the trail of light. Animation means which frame of what kind of animation is currently being drawn.
The big key element in this new design is the Receiver. I couldn't think of a better name at the time and this may be renamed when I do, but a Receiver is responsible for determining the next direction of the player. In LR 1.0, the Android button hooks were tied directly to a field that held the next direction of movement until the physics update could pick it up. It also had the AI code in the same big game class and assumed that any player that was not Player 1 was AI. That won't work for the new requirements so I'm designing this to make 2 policies: 1) The Receiver is entirely responsible for setting the next direction of a Player. 2) If there is no Receiver, the Player shall always continue (interpolate) on its trajectory.
Does policy 2 make sense? When would we not want a Receiver? Wouldn't that player always end up in the wall if it started North and continued without change?
Let me start by answering that there are several types of Receivers. The Player Receiver is directly tied to the buttons and touch. When a player hits up, that receiver sets the next direction to North. Simple enough. The AI Receiver is abstract and so there can be several different AI implementations. I'm doing 3, which are Easy, Medium and Hard. The different implementations may be better at playing than one another but they all end up outputting only one thing: The next direction for their Player. The network receivers are the most confusing. I'll explain the way multi-player games will work and then get back to them.
In Light Racer, you will be able to host a game or join a game. There can be up to 3 players for the regular game and 4 players for LR3D. The clients each "estimate" what is happening but the host is the real authority. One could say that the actual, authoritative game runs on the host. This means that when the host decides there was a crash, there was a crash. This is an easy strategy to follow because the rule is that the host's world always overrides anything on the client.
Let's discuss the following example:
In this example, there are 3 players. One player is the host of the game, the second is a client and the third is an AI player. The configurations are different in that the host uses a Human Receiver for the person playing, a NetHost Receiver to pick up the input from the remote player and an AI Receiver to control the AI. Every time it ticks and updates the world, it packages the world up and sends it to the client. The client unpackages the world and overwrites its current world. This is because the host is always the authority. The client then processes input from its one Receiver, which is a NetClient Receiver. The NetClient Receiver is dual-purpose. It has a Human Receiver so it can take button presses and touch events from the real player and that will set the next direction, but then it also immediately sends that over to the host via the network connection.
If you can imagine that happening around 30 times per second, you can see why the Client World does not need Receivers. Its World is always being updated by the host and the Players in that world are located and on the trajectory that the World dictates. This means that the client simply tells the host what it would like to do and draws what it thinks is happening. The host processes this and tells the client what actually happened. So long as the updates are fast and consistent, this will work. Even if there are small stutters, the interpolation code will take care of it and things will still look mostly smooth.
In a few weeks I will address the issue of 2D and 3D code both running from the same codebase.