|
By Michael Powers, December 2006
|
|
|
In Part 1 of this article, I examined the obstacles to writing real-time multiplayer games for mobile devices, and proposed design solutions to surmount them.
This part applies those ideas to the development of a simple prototype of a multiplayer networked game. The intent is to provide the scaffolding from which to jump-start your imagination and help you on your way to shipping your own multiplayer blockbuster.
The article will walk through design and architecture of the game; you should download the code to follow along. You can also play the game right on your desktop, with the mpowerplayer webstart emulator.
Contents
Concept
Every game begins with a concept. The genre and backstory determine the user's first impressions and gameplay expectations. For mobile games in particular, the user makes conclusions based on very few inputs: the name of the game, an icon, a couple of lines of text, and preferably a screenshot. The concept must be straightforward and easy to convey.
In my game, each player commands a big spaceship like one you'd see in a Star Trek series. Ships will maneuver for position and fire torpedoes to destroy their enemies. Because one of the primary design challenges is reducing user-perceived latency, I chose to represent a slower-paced tactical battle rather than a quick-reflexes dogfight.
Unless you have the budget for a big marketing blitz, the JAD file may be your only communication with the user. Many MIDlet provisioning systems populate their user-facing catalogs with the information contained in the JAD file. Unfortunately, there's not much room to make an interesting impression. To turn prospects into buyers you need to choose your words wisely.
Listing 1: SpaceWar.jad
MIDlet-1: Battlecruiser, SpaceWar32.png, SpaceWar
MIDlet-Name: Battlecruiser
MIDlet-Icon: /SpaceWar32.png
MIDlet-Vendor: Sun Microsystems
MIDlet-Version: 0.1.1
MIDlet-Description: From the bridge of a Federation Battlecruiser,
defend the sector against all enemies!
MicroEdition-Configuration: CLDC-1.0
MicroEdition-Profile: MIDP-2.0
|
While the name of my MIDlet project is SpaceWar, Battlecruiser is a name that better captures the idea of the game, and that's what goes in the first field of the MIDlet-1 entry. MIDlet-Icon lets me provide an icon that I hope looks more like a big starship and less like a small fighter. MIDlet-Description lets me specify a short text description to try to set the mood.
User Design
Simplicity is king in user interface design, and doubly so in mobile gaming. Some of the best-received games of late use only one button to control all aspects of gameplay. Battlecruiser will not fall into that category, but the design will be as simple as possible.
To suit the casual-gaming habits of the mobile consumer more closely, the design must make it easy to enter and exit the game, and play for short bursts of time. Battlecruiser achieves this goal by omitting the login process completely. When the game is launched, it immediately connects to the game server without requiring any user ID or password, or any other input. To disconnect, the user simply exits the application.
Once the user is in the game, the controls must be immediately intuitive. The specification of the Mobile Information Device Profile (MIDP) guarantees four directional keys and a fire button, so those are the only controls used. Up and Down accelerate and decelerate the ship, Left and Right set the desired direction, and the Fire button launches a torpedo. The game doesn't use the A, B, C, or D buttons, and the only option the Command menu needs is Exit.
Simplicity affords another important advantage in device compatibility. Given the variety of physical design in devices that support MIDP, any assumption about the placement and operation of a button or command menu option may not hold true for every device; you can unintentionally create an awkward or impossible interface for some users. Keeping it minimal ensures your application will have the largest possible audience.
Network Architecture
The two chief constraints on network gaming on any platform are latency and bandwidth, and their impact looms even larger in the mobile environment. The game design must do its best to hide the effects of delayed network messages, and minimize traffic on the wireless network.
Here's how Battlecruiser achieves these goals: Players issue commands to their ships, which then rotate and accelerate to follow the specified courses. From a UI perspective, the delay between the player setting the course and the ship setting into motion effectively masks potential network delays. Still, quick feedback to input reassures the user, so the game draws the plotted course to the screen immediately, even before the ship starts to move. In all cases, the local application feels responsive even if the network isn't.
Figure 1: Objects in Space |
Part 1 of this article described a means to reduce the bandwidth required by a multiplayer networked game, a navigation technique called dead reckoning. Battlecruiser adopts a very basic version of dead reckoning:. The game is a distributed simulation running identically on each client. The server doesn't calculate the full game state and communicate it to clients. The clients simply exchange changes in the simulated objects' state, such as modifications in trajectory, and each client calculates the objects' new positions off-network. Reducing the number of messages reduces the reliance on the network and therefore reduces the impact of poor network latency.
Implementation
While Battlecruiser is not a sophisticated game, it does ably demonstrate how, using the Java platform, you can easily put together a graphical, multiplayer, networked, cross-platform game spanning both client and server. The Battlecruiser client is a standard MIDlet, the server is a standard servlet, and the entire project comprises only ten new classes.
The inheritance diagram in Figure 2 illustrates which classes extend standard Java classes and which ones stand on their own.
Sub Head
Figure 2: Class Inheritance |
As you can see, half of the classes have no MIDP dependencies at all. This portion of the code base can run on any Java virtual machine1. Whether your concern is for game rules or business processes, separating the data and behavior from the presentation affords flexibility down the road, and lets you reuse your work in ways you might not currently imagine.
The next diagram shows the runtime dependency relationships between the primary actors in the application. The arrangement loosely follows the Model-View-Controller pattern.
Sub Head
Figure 3: Class Dependencies |
The model layer consists of Space, Entity, and Plot. These classes are all self-contained "plain old Java-language objects," with a key implication:the model classes can run on the mobile client as well as the server.
The view layer is the part of the application that interacts with the user. Battlecruiser's only screen is SpaceCanvas, a subclass of GameCanvas. It uses EntitySprites to render each visible entity in Space and updates the entities' Plot in response to user input.
The remaining bit is the Synchronizer, which connects to the game server and keeps the local model in synch with the models running on the other clients across the network. Synchronizer could be implemented in many different ways, but this implementation polls a servlet over HTTP at regular intervals.
The SpaceWar MIDlet, for its part, instantiates all of these objects, connects them to each other, then basically gets out of the way. It has two other important responsibilities, however.
MIDlets must correctly implement the life-cycle methods: startApp(), pauseApp(), and destroyApp(). Many MIDlets today seem to neglect proper implication of the pauseApp() method, a shortcoming that quickly becomes evident on modern multitasking-capable MIDP devices. SpaceWar starts the repaint timer and immediately connects to the server in startApp(). Note that this method is called both when the application is launched and when it resumes operation after being paused. The pauseApp() method stops the repaint timer and disconnects from the server, and destroyApp() does the same.
Because the game connects to the server without any visible user ID or registration, on first launch SpaceWar generates a permanent identifier code for the player and saves it in a record store. This identifier needs to be unique across all clients that might be playing the game now or any point in the past - a difficult requirement when we haven't even connected to the server! While there are very sophisticated approaches to this problem, SpaceWar's "good enough" approach is to generate a pseudo-random ID based on the current time.
With a persistent and unique identifier, the game play can seamlessly survive network outages and abrupt terminations of the client. The design allows players to launch the game quickly, and after any disconnection resume play exactly where they left off, an essential feature for a mobile game.
The Model
The model layer is the core of the game engine, codifying the game rules and physics, and implementing the necessary architecture to support dead reckoning.
How does it work? Because each client is assumed to have exactly the same implementation of the model layer, a game session is actually a distributed simulation running simultaneously on all of the clients.
Player input is translated into a series of events for future points in time. These events are sent to all other clients so that each instance of the simulation has the same record of events. As the game clock ticks, all clients have identical state and all players see identical behavior.
Remember that the model does not record the specific position of each entity at each point in time. Instead, the model records only past and future events - course changes, acceleration, deceleration, weapons fire - and can therefore calculate the position of any entity as of any given point in time.
This approach is important because events may be delayed or received out of order. With Battlecruiser's architecture, inserting delayed or disordered events into the local event history automatically causes the rest of the local simulation to correct itself, bringing it back into synch with the other clients.
A Plot represents one of these events. It is a simple data structure that holds the state of an entity at a point in time. The entity's state includes not only its status and location but also its present trajectory, represented as a direction and magnitude, and its desired trajectory, if different from the current trajectory.
Entity maintains an ordered collection of plots and provides methods for obtaining the state of the entity. Given a point in time, the entity will search through its plots for the most recent one before that time and extrapolate to find the current position.
In most cases, this extrapolation is a straightforward calculation: multiply the trajectory by the time difference and add it to the last plotted position. The calculation becomes more complicated when the desired trajectory differs from the current one.
Because the spaceships are assumed to be large, lumbering cruisers rather than agile fighters, the game sets limits on their rates of acceleration and rotation. When the player sets course and speed, these are represented as a Plot with a desired direction and magnitude at a point in the future. On screen, the ship turns and accelerates gradually, over the span of several seconds.
This mechanism is not only a welcome bit of realism, it's essential, because it dampens the effects of latency. Even with several seconds of network delay, the local player notices course changes made by a remote player as little more than a hiccup or minor jump in an enemy ship's position.
Two subclasses of Entity, named Ship and Shot, represent the ships piloted by the players and the torpedoes they launch, respectively. These classes serve mainly to signify entity types, and differentiate the rates of acceleration and rotation between them.
A word about time: Everything in the game simulation relies on a shared timeline. All Plots have a time at which they take effect. I call this unit of time a "tick." How much physical time corresponds to a tick has no importance to the model. It matters only when clients render the model in real time.
The View
The view classes provide the user interface, rendering the current state of the model on the screen, and updating the model in response to the player's actions. Because this MIDlet targets MIDP 2.0 devices, the view implementation uses the Game API to reduce vastly the amount of code needed to draw to the screen and react to user input efficiently.
SpaceCanvas is Battlecruiser's only screen, shown immediately at startup. It remains on screen until the user selects the Exit command or otherwise instructs the application to pause or quit.
I made SpaceCanvas a subclass of GameCanvas, even though it does not make explicit use of getKeyStates() or other GameCanvas-specific functionality, for one very good reason: An instance of GameCanvas is given its own graphics buffer and, on some devices, drawing may be optimized in ways unavailable to the Canvas superclass. Because Battlecruiser uses other parts of the Game API, GameCanvas is around anyway, so there's no reason not to take advantage of it.
SpaceCanvas maintains a collection of EntitySprites: one for each entity contained in Space. It uses a LayerManager to maintain the sprites and draws a tiled background image behind it to give the illusion of motion.
When SpaceCanvas is asked to repaint itself, it first determines the current time and updates each sprite with the current position of each corresponding entity for that time. If Space contains new entities since the last repaint, new sprites are created to render them.
SpaceCanvas then instructs the layer manager to draw the background and all of the sprites that fit into the current view port, with the player's own ship in the center. Finally, it draws the "heads-up display," used to indicate plotted course and direction, drawn around the player's ship in the center of the view.
Battlecruiser employs a simple and common repaint strategy. Given a desired number of frames per second, the MIDlet sets up a Timer and schedules a recurring task with an interval sufficient to ensure the repaint() method is called the desired number of times each second. With this approach, there's no harm in specifying a very high frame rate. When repaint() is called more frequently than the hardware can paint the screen, the calls are coalesced, so there's no real harm. Some games eschew the timer entirely and start a thread that calls repaint() as often as it can, but it's really best to be more frugal with the meager processor you're given.
Another word about time: SpaceCanvas is responsible for showing the user the current state of the model at the current time, animating it as time passes. All clients must therefore agree on how much real time corresponds to a single game tick. In this case, I've decided that each tick corresponds to 100 milliseconds, which is large enough to avoid worrying about varying degrees of timing precision across devices.
Because the simulation runs at no more than ten ticks per second, it makes no sense to paint the screen more frequently. The sample code defines these constants as static variables so you can experiment with different approaches. Interpolating object positions between game ticks to yield smoother animation at higher frame rates is left as an exercise for the reader.
The view layer's other primary responsibility is handling user input. SpaceCanvas implements keyPressed() and keyReleased()UP, DOWN, LEFT, RIGHT, and FIRE. It passes these commands along to the adjustTrajectory() method of the EntitySprite that represents the player's ship. When that sprite next paints itself, it draws the graphical indicators that immediately show the user that the input was received.
SpaceCanvas also creates a Plot to represent the user's command to the model and to send it to other clients, but not immediately. To avoid creating too many network messages, SpaceCanvas employs a Timer to coalesce several key events into a single Plot. The timer starts with each call to keyReleased()keyPressed(). After a brief delay in key events, the timer eventually fires and calls doPlot() to construct the current plot and record it in the model with postEventForID().
To reduce latency further, the doPlot() method creates the plot with a time that is ten ticks into the future. This delay enables the event to be transmitted to other clients before it takes effect - without giving the player an impression of poor response. Remember that the immediate feedback of the sprite's heads-up display mitigates the perception of delay, until the model update becomes apparent about a second later.
The Synchronizer
The Synchronizer class is responsible for shuttling events between the local simulation and the outside world. It connects over the network to a game server, and synchronizes changes to and from the local Space instance.
Different implementations of Synchronizer may choose from a wide array of communication options for network transport, but the simplest and most compatible approach is to use MIDP's built-in HTTP facilities. While HTTP lacks some capabilities available in other protocols, it has one big advantage: it's supported by all MIDP devices and nearly all network operators.
Synchronizer repeatedly connects to the server after a fixed interval, sending any new local plots and receiving plots generated by other players. Polling in this way sacrifices efficiency and performance but yields reduced complexity and greater compatibility. For each connection to the server, the additional headers and data required by the HTTP protocol can tax the resources of both the client and the server, and hamper the ability to scale up to a large number of simultaneous users. Other connection modes, like raw sockets or datagrams, are greatly more efficient but aren't supported by most carriers or devices. If your resources allow, you can customize the implementation of Synchronizer to take advantage of the networking capabilities of the more sophisticated devices, by factoring the network connectivity functionality into a separate class.
When a player uses the navigation keys to maneuver, the generated Plot is received by Space in the postEventForID() method, and placed on a queue of pending events. The next time Synchronizer connects to the server, it removes the pending events from the queue and sends them as part of a POST request to the server. If no events are pending, Synchronizer sends a GET request. In both cases, the server receives a single parameter, a special numeric ID that the server uses to determine what events need to be sent in the response.
The response contains a simple list of events that Synchronizer converts to Plots and inserts into Space. Each Plot arrives alongside the ID of the entity to which it belongs. If the ID is unrecognized, Space creates a new entity, a ship or a torpedo, to associate with the event. Part of the ID contains the entity type, so Space can know what subclass of Entity to instantiate, Ship or Shot. If the event is an exit event, the corresponding entity is flagged for removal from Space.
The Game Server
Essentially, SpaceServlet is a message queue. Clients post their own events to the queue, and retrieve the other clients' events from the queue. The prototype implementation is pleasantly concise. It simply maintains an array of raw serialized plots and a parallel array of the entity IDs to which the plots belong.
Recall that when a client connects, the server receives a special numeric parameter; this is actually an index into the array of plots. The server copies all plots after that index into the response and sends back the index of the last plot sent. When that client reconnects, the request contains the old index and the server again responds with all subsequent events. Because the connections are stateless, the servlet does not need to maintain an individual session for each player. This design increases the number of simultaneous users that the server can support.
While this approach is uncomplicated, it is also very insecure. Anyone that can observe the protocol can easily insert spurious events and disrupt the game on all of the clients. For a mobile game deployed and sanctioned by a carrier, where all clients are coming from a closed network and the application is signed and provisioned through trusted sources, security may not be a concern.
For deployments publicly accessible over the open Internet, however, you'll need to build in countermeasures to thwart cheaters and other nefarious malcontents. Your options include encrypting the transport with HTTPS or a custom encryption layer, signing the network messages, or requiring a signed client application.
Another security measure is to perform input validation, which can be as sophisticated as you have time to implement. A useful approach is to run an instance of the model on the server to determine whether an incoming plot is a legal move for the associated entity.
Running the model on the server can provide additional advantages. Depending on your game dynamics, you may be able to optimize your network message flow. Suppose, for example, some clients don't need to receive events from all other clients. Going further, the approach you've seen here can support computer-controlled clients running on the server. If you decide to implement artificially intelligent players, you'll find the model's built-in capability to project the future positions of objects highly convenient.
Summary
This article illustrates a design and approach to help you rise above the common obstacles to bringing a successful real-time multiplayer mobile game to market. To increase your likelihood of success, make your game simple in concept, and easy to pick up and put down. The game design should not be overly sensitive to the effects of network latency, and the amount of transmitted data should be kept to a minimum. Employing dead reckoning techniques with Java's cross-platform capabilities offers a convenient and appealing solution to these difficult design constraints.
About the Author
Michael Powers is Chief Technology Officer of Mpowerplayer Inc., a leading provider of technology solutions to the mobile gaming industry. You can play the game featured in this article and many others from leading publishers, right on your desktop, with the free Mpowerplayer emulator, available at http://mpowerplayer.com/.
1 As used in this document, the term "Java virtual machine" or "JVM" means a virtual machine for the Java platform.
|