Chapter 20. Connecting Out: Wireless Networking

Above all else, a mobile phone's true purpose is to connect people together over vast distances. Single player games on phones are well and good, but the tiny graphics window and puny processors will never compete with the likes of Microsoft's Xbox, Sony's PlayStation 2, or Nintendo's GameCube.

Rather, the mobile games that are most likely to be successful and awe-inspiring will involve lots of players cooperating, competing, and sharing an experience. A well-done wireless game can bring people together in ways previously unimaginable.

J2ME Networking Overview

In the world of Java Standard Edition, large and intricate java.io.* and java.net.* packages are used to great effect. These packages contain pretty much any type of networking class you want: Socket, DatagramSocket, ServerSocket, and so on. Each class has different methods and different ways of being used.

In the world of J2ME, however, we don't have the luxury of being quite so complete. For starters, we have no idea what type of network transport protocol a phone is using. Devices that work over circuit-switched networks can use streaming always-on connections such as the Transport Control Protocol (TCP). However, packet-switched networks may only be able to handle their network data in discrete, non-guaranteed packets, using a protocol such as the User Datagram Protocol (UDP). What's a poor phone to do?

The CLDC's Connection interface was created to be as general as possible. The Connection class is a catch-all that can, in theory, handle pretty much any network connection. A special class known as the Connector can tap into any CLDC class that extends from the Connection interface.

The Connection interface is an ultra simple, ultra-generalized networking class with only two methods: open() and close().

There are several more specialized interfaces that extend from Connection. These are illustrated in Figure 20.1.

The Connection J2MEnetworkingConnection interfacenetworkingJ2MEConnection interfacewireless networkingJ2MEConnection interfaceConnection interfaceinterfacesConnectioninterfaces.

Figure 20.1. The Connection interfaces.

  • InputConnection—. This points to a device from which data can be read. Use the openInputStream() method to return an input stream.

  • OutputConnection—. This points to a device from which data can be written. Use its openOutputStream() method to talk to the outside world.

  • StreamConnection—. Combines input and output connections. The StreamConnectionNotified interface waits for a connection to be established and then returns a StreamConnection.

  • ContentConnection—. Extends StreamConnection and deals with metadata about the given connection.

  • DatagramConnection—. A point that can send or receive datagrams.

Every Connector's open method takes in a String with the familiar syntax: “protocol:address;parameters”. For example, to open up a typical HTTP connection

Connector.open("http://java.sun.com/developer");

to open a socket

Connector.open("socket://108.233.291.001:1234");

and to open a datagram connection

Connector.open("datagram://www.myserver.com:9000");

For example, to connect to a server, simply have the MIDlet use the Connector class:

Datagram dgram = dc.newDatagram(message, msglength,"datagram:// www.myserver.com:9000");
dc.send(dgram);
dc.close();

You can even theoretically connect to a serial or infrared port if your phone or J2ME device supports the interface:

Connector.open("comm:0;baudrate=9600'),

Or even open a local file, if the device has a file system:

Connector.open("file:/myFile.txt");

Get the idea?

When you call the Connector class, it sniffs out the protocol you are using and looks for an applicable Connection class to use. This makes it exceptionally easy to swap out one protocol for another—merely change the String passed to the open() method.

NOTE

The CLDC itself does not actually implement any protocols. The MIDP is required to support the HTTP protocol, but not necessarily sockets or datagrams. These protocols may or may not be supported by specific phones. As such, it is recommended that you use HTTP for all your game communications, so that the same game will work on pretty much any MIDP-compliant phone.

MIDP Networking

The MIDP specification makes it extremely easy to pass data back and forth using HTTP. To do this, MIDP adds the HttpConnection interface to the Connector suite, allowing for HTTP protocol access. The HttpConnection interface, extending from javax.microedition.io.ContentConnection, handles everything you could want to request or respond to an HTTP connection.

Every Motorola i85s phone, for example, has its own static IP address. Having one phone communicate with another is only a matter of simple peer-to-peer networking. Have one phone connect to the other's IP address using HTTP and communicate away!

More often, though, your phone will connect to an outside server machine. This server can be used as a gateway for almost any type of game traffic or other network communication. For instance, a gateway can be set up to access an entire database of sports scores, then stream only the latest requested scores to a MIDlet as part of a real-time sports fantasy game.

A Little Info About HTTP

The Hyper Text Transport Protocol (HTTP) is the protocol that you use each time you surf the Web. It is a request-response protocol that works like this:

  1. The client sets up the connection. This is known as Setup mode.

  2. The client sends a request to the server, asking for a specific piece of data. It can add all sorts of request headers to this request.

  3. The connection to the server remains open. This is known as Connected mode.

  4. The server interprets the request and then sends a response back to the client. The response might also have many headers, helping the client interpret the response body.

  5. The connection is then closed. This is known as the Closed mode.

HTTP Setup Mode

So, to set up a HTTP connection on your MIDP client, create the connection

HttpConnection c = (HttpConnection)Connector.open("http://www.myserver.com/");

You can then change the request method or alter a request property. By default, the request method is GET, which means that all parameters are passed in along with the URL.

You can also use the POST method, which passes all data and parameters as a separate chunk, thus enabling you to send much bigger packets of data:

c.setRequestMethod(HttpConnection.POST);

Use the following to set some of the HTTP headers:

c.setRequestProperty("User-Agent","Profile/MIDP-1.0 Configuration/CLDC-1.0");
c.setRequestProperty("Content-Length","100");

At any time, you can call various methods to get the status or other information about the HTTP connection. For example

  • getURL—. Gets the full URL.

  • getProtocol—. Gets the protocol from the URL.

  • getHost—. Gets the URL's host.

  • getPort—. Gets the port from the URL.

  • getFile—. Gets the file portion of the URL.

  • getRequestMethod—. Gets the current request method. The default is GET.

  • getRequestProperty—. Gets the value of a request property.

Making the HTTP Connection

There are several methods you can call to actually try to connect:

  • openInputStream—. Read text from the HTTP connection.

  • openOutputStream—. Write text to the HTTP connection.

  • openDataInputStream—. Read binary data from the HTTP connection.

  • openDataOutputStream—. Read binary data from the HTTP connection.

  • getLength—. Get the length of the current packet of data.

  • getType—. Get the type of the current packet of data.

  • getDate—. Get the date when the current packet of data was created.

  • getExpiration—. Get the expiration date of the current packet of data. This can be found in the “expires” line of the HTTP header.

Closing Out

To close your HTTP connection, use this method:

  • close—. Inherited from the Connection interface. Closes out the connection.

HTTP Example

A typical example of sending some data to a servlet via POST might look like this:

HttpConnection c = null;
InputStream is = null;
OutputStream os = null;

c = (HttpConnection)Connector.open(url);

// Set the request method and headers
c.setRequestMethod(HttpConnection.POST);
c.setRequestProperty("User-Agent","Profile/MIDP-1.0 Configuration/CLDC-1.0");
c.setRequestProperty("Content-Language", "en-US");

You can then send your data along using an output stream:

// Write your data to the output stream
os = c.openOutputStream();
os.write("data");
os.flush();

Response Code

The MIDlet usually asks for the response code to ensure that the data was returned correctly:

int rc = c.getResponseCode();
if( rc == HttpConnection.HTTP_OK ) {
// we're good... continue
} else {
// tell the user about the error
}

In some cases, the response code might be valid, but not HTTP_OK. For example, you might get HTTP_MOVED_TEMP, which means that the servlet or Web page you requested might have redirected you to another page. You should grab the new URL from the header and open a new connection:

int rc = conn.getResponseCode();

switch( rc )
{
 case HttpConnection.HTTP_MOVED_PERM:
 case HttpConnection.HTTP_MOVED_TEMP:
 case HttpConnection.HTTP_SEE_OTHER:
 case HttpConnection.HTTP_TEMP_REDIRECT:
  url = conn.getHeaderField( "Location");
  if( url != null && url.startsWith("/*" ))
  {
    StringBuffer b = new StringBuffer();
    b.append( "http://" );
    b.append( conn.getHost() );
    b.append( ':' );
    b.append( conn.getPort() );
    b.append( url );
    url = b.toString();
  }
  // Close the old connection
  c.close();
  // You should open a new connection here
  break;
}

Reading In Data

In any event, eventually you can read in the returned data. You can read in the data as raw bytes using an input stream.

Whenever possible, you should use the content length header information when reading in data. If you try to read more bytes than were sent, the application might just wait forever waiting for new data to come down the pike.

However, you should also prepare for the situation in which you're not sure how much data to expect. In that case, just read in one byte at a time until you get -1, indicating that there are no more bytes to be read.

// Open an InputStream to read in the HTTP headers.
is = c.openInputStream();
// Get the length and process the data
int len = (int)c.getLength();
if (len > 0)
{
  byte[] data = new byte[len];
  int actual = is.read(data);
  // do something with the data here
}
else
{
  // If we don't know the length, read in the data
  // one character at a time and add it to a byte array.
  ByteArrayOutputStream tmp = new ByteArrayOutputStream();
  int ch;
  while ((ch = is.read()) != -1)
    tmp.write( ch );
  data = tmp.toByteArray();
  // do something with the data here
}

On the other hand, you can read in your data as a data input stream. This lets you read in primitive Java types (such as strings, integers, and so on) in a machine-independent way. The servlet must use a data output stream to write data accordingly:

DataInputStream is = new DataInputStream(c.openInputStream());
String msg = is.readUTF();
// Do something with the data

Closing Down Cleanly

You can continue to write and read data if you so desire. Just be sure that the connection is of the “Keep-Alive” variety. More information about keeping connections alive can be found later in this chapter.

In the end, be sure to close all open streams and connections:

if (is != null)
  is.close();
if (os != null)
  os.close();
if (c != null)
  c.close();

Working Around HTTP's Limitations

HTTP is known as a half-duplex protocol. That means you cannot transmit information in two directions at the same time. If your game is advanced, with unpredictable data packets constantly coming in and going out, you'll want to use a full-duplex protocol. But what if your phone only supports HTTP?

Multiple Connections

One way to achieve full-duplex communication is by having your MIDlet client create multiple connections—one that sends data to the server and one that stays alive, retrieving server data.

The first connection should use chunked transfer encoding. The server creates an open connection and assigns it some sort of unique ID. This ID is sent down to the client.

The client can now create a client-to-server connection with an incredibly large content-length heading, passing in the proper ID. The server can then handle request data as it flows in from the client and channel an appropriate response to the open server-to-client connection.

Some servers and proxies may not be able to handle a request with a long content length heading, buffering the request and waiting for the request to be completed. In this case, the client can break each message into a chunk and send it as a new, individual request. The client should send the ID along with each request, either as a custom header element or as part of the payload. The server can then parse this ID number and send the appropriate response back to the client.

The Power of the Proxy

Of course, you can write your game server using any networking protocol you want. You can tap into the game server via an HTTP proxy. A proxy is a servlet or other server-side component that reads in HTTP messages sent from your MIDlet and translates them into another protocol which it then transmits to the game server. Likewise, a proxy will take server messages and translate them into HTTP messages, queuing them up and sending them out to your MIDlet as necessary.

Using a proxy will add extra latency to your game's communications. But it will also allow you to write games that support different network protocols. For example, devices that support datagrams can use them. If a ConnectionNotFoundException is thrown, the game can revert to using HTTP via a proxy instead.

Setting Up Your Game Server

Your remote game server can, of course, be another mobile device. This lets you achieve true peer-to-peer gameplay.

To accomplish this, create an endless loop that listens to a port and waits for some data. If you are sure your target device supports datagrams, for example, you could write something like this:

int receiveport = 91;
DatagramConnection dc = ( DatagramConnection)Connector.open("datagram://: "+receiveport);
while (true)
{
   dgram = dc.newDatagram(dc.getMaximumLength());
   dc.receive(dgram);
   reply = new String(dgram.getData(), 0,dgram.getLength());
}

Alternatively, you could write a dastardly simple server in Java Standard Edition (or any other language), running as an application on any network-enabled PC.

For instance, the meat of your server could handle datagrams as follows:

DatagramSocket receiveSocket = null;
DatagramPacket receivePacket = new DatagramPacket(bytesReceived, bytesReceived.length);
receiveSocket.receive(receivePacket);

For the sake of this chapter, however, we will focus on HTTP and use a Java servlet to handle the HTTP communications.

Servlets are perfect for dealing with HTTP, because they have been designed from the ground up for request-response connections.

To use a servlet, you must be using a J2EE Web server, such as Resin, Tomcat for Apache, Weblogic, SilverStream, and so on. You can also configure most other Web servers to support servlets. Tomcat is a free, open-source servlet engine. You can grab it from http://jakarta.apache.org/site/binindex.html.

Data Format

Your game packets can be sent in any format you want. Obviously, you want to try to keep your data as small as possible.

Doing Your Own Packing

You can pack a lot of information into a byte. For example, you can fit 8 Boolean flag values into each of the 8 bits. The following code line represents false, true, true, false, false, false, true, true:

01100011

To set this, you could use a function like the following code fragment, passing in an array of 8 Booleans:

byte setFlag(boolean[] flag)
{
  // temp starts off as 00000000
  byte temp = 0;
  for (int i=0; i<8; i++)
  {
   // Set the last bit to 1 if true
    if (flag[i])
       temp += 1;
    // Shift the bits over to the left
    temp = temp << 1;
  }
  return temp;
}

Then you can read the values by masking out bits using the bitwise and command (&). For example, to see if the seventh bit is true or false, you can mask your byte with 01000000 (64), and see if the result is 0 (false) or 64 (true):

01100011 & 01000000 = 01000000
(99 & 64 = 64)

You can also shift the bits over a set amount and compare the first bit. For example, this method returns whether a given flag is true or false:

boolean getFlag(byte b,int location)
{
    return (((b >> location) & 1) == 1);
}

For the most part you shouldn't need to roll your own bit-packing routines. The DataInputStream and DataOutputStream, discussed a bit later in this chapter, do a decent job.

XML

Many people adore eXtensible Markup Language (XML), which formats documents in a highly organized and readable way. XML has some great advantages. For example, it's really easy to figure out what's in this game message:

<move><sprite>car</sprite><x>10</x><y>15</y></move>

However, XML is very verbose and will create ultra-heavy data packets. There are, however, ways to compress and otherwise optimize XML data.

If you want your MIDlet to parse XML data, you can choose various types of parsers:

  • Validating—The document is checked against a DTD. This requires a lot of extra code and time to validate. It is not recommended that you ever use a validating XML parser.

  • Non-Validating—The document is just read in.

  • Single-Step Document Parser—This will parse an entire document and create a document-type-definition (DTD) object that contains a sense of all nodes, children, and branches. The root of the tree is called kXMLElement, and each node is an instance. You can grab nodes using methods such as getChildren(), getTagName(), and getContents(). This type of parsing takes tons of time and uses up scads of memory because the entire document must be kept around.

  • Incremental—This will take in a line and parse it, putting the data into temporary variables. A sense of the entire document is not retained. This is the smallest and fastest way to parse XML.

There are several good non-validating parsers available for MIDP.

To parse a document incrementally using kXML, just use code similar to the following:

InputStreamReader xmldata = new InputStreamReader( c.openInputStream());
XmlParser parser = new XmlParser(new InputStreamReader(xmldata));
try {
    boolean keepParsing = true;

    while( keepParsing )
    {
        ParseEvent event = parser.read();

        switch( event.getType() )
        {
            case Xml.START_TAG:
                // handle the start of a XML tag
                break;
            case Xml.END_TAG:
                // handle the end of a XML tag
                break;
            case Xml.TEXT:
                // handle the text inside a tag
                break;
            case Xml.END_DOCUMENT:
                // document done
                keepParsing = false;
                break;
        }
    }
}
catch( java.io.IOException e ){ }

Encoding with DataOutputStream and DataInputStream

Perhaps the easiest way of sending and receiving data is to use Java's predefined data types. For example, nothing could be easier than sending an integer, a String, and a boolean, and then reading them directly back in.

The DataOutputStream and DataInputStream classes make this immensely easy. For example, to write out your variables

DataOutputStream os = new DataOutputStream(c.openOutputStream());
os.writeBoolean(true);
os.writeUTF( "Hello World!");
os.writeInt(5);
os.close();

and then to read them in

DataInputStream is = new DataInputStream(c.openInputStream());
boolean b = is.readBoolean();
String hello = is.readUTF();
int t = = is.readInt();
is.close();

Making a Multiplayer Car Racing Game

So, let's create a game here!

Obviously, it would be nice to modify our car racing action game to have two cars racing on the same track, each trying to reach the finish line first. The latency of most mobile networks, however, makes such a game quite impossible.

Games that require immediate knowledge of where opponents are and what they're doing just don't make sense over low-latency connections. A shooting game just isn't much fun when it takes a second or more for the news of your gunshot to reach your opponent!

The types of multiplayer games presently possible with today's wireless networks are as follows:

  • Turn-based games—Almost any board or card game is possible on MIDP.

  • Games that read from the same data source—Many games, such as fantasy sports games, involve people competing against each other in a way that doesn't involve direct interaction. You make your decisions, your opponent makes her decisions, and the game is kicked into action.

  • Centralized high-score lists, ratings, and rankings.

  • Games where the interaction with other players isn't graphical—for example, in our Micro Racing game, you drive the track and win money. You can then spend your money online and buy or sell parts for your car.

  • Simple chatting and instant messaging applications.

All this being said, there is a clever way to create a two-player racing game:

  1. Two players start the game and log into a server.

  2. Both clients show two cars on the track.

  3. The server sends a “start game” message to both clients.

  4. When the game starts, the client uses artificial intelligence to move the opponent's car slowly along the track.

  5. Meanwhile, the client sends information to the server about where it is and how much time has elapsed.

  6. The server then sends information about the opponent's car as often as possible. Your opponent's car “leaps” forward or backward on the track depending on where it actually is.

Although this system is workable, it is definitely clunky and unsightly. Instead of trying to cheat slow latencies, we're going to use this chapter to create a car part trading system that would be cool and useful to use with the present limitations.

Design the System

The first step in building the multiplayer aspect of your game is to look at the game design and figure out the minimum amount of data that needs to be sent back and forth, which in our case is the following:

  1. You log into the server and enter the Garage. You send in your username.

  2. You are sent back a list of all the items available for sale.

  3. You are also sent a list of others who are currently hanging out in the Garage.

You have two choices: Buy It! or Sell It!

  1. If you decide to buy, you can pick the item you want. A more detailed description is shown to you. You can then click the Buy It! button to make the purchase.

  2. If you have enough money, the purchase is made. Money is deducted from your account and you now own the new item. It is automatically installed onto your car (saved into the game's data record). The next time you race, you will reap the advantages of this new engine, tire, or weapon.

NOTE

If this game was commercial, the Garage might charge a nominal maintenance fee in real dollars for installing your item.

  1. If you decide to sell, you are a shown a detailed list of all your items. You check the items you want to sell. You can then indicate how much money you want to charge for it. You then hit the Sell It! button. As long as you stay logged into the Garage, others users can buy this item.

  2. When you log off, any of your goods that didn't sell are taken off the market.

Special Considerations

Because we're going to be using HTTP as the transport medium, several considerations must be kept in mind:

  • Unlike a socket, an HTTP connection is generally not kept alive. Rather, the client must “pull” info from the server as often as it needs it. To figure out whether any of your items have sold, you will need to poll the server constantly to check on the status of your goods. If anything has been sold, you are notified. That item is removed from your inventory and the purchase price is added to your account.

  • The connection is stateless. This means that every time you talk to the server, you need to tell it who you are. This is called session tracking.

Polling

Polling can be accomplished by creating a thread, having the thread send a request to the server, and then sleeping for a given number of milliseconds.

In the land of MIDP, however, it is much more efficient and convenient to use a Timer and a TimerTask.

Create the timer as follows, giving it a pause of 10 seconds:

timer = new Timer();
CheckInTimer ci = new CheckInTimer();
timer.schedule(ci,(long)10000,10000);

The CheckInTimer class can look like this:

class CheckIn
{
  CheckIn()
  {
    ConnectServer("checkin",null,this);
  }
}

How long the timer should wait to trigger each server call depends on your game design and on the target network. In general, the faster your timer, the more responsive the game—but realistically, wireless networks cannot handle multiple connections spaced less than one second apart.

Keeping HTTP Connections Alive

By default, HTTP connections are kept alive. This means that the same connection will be used for multiple requests. This is useful for any situation where the client will repeatedly send requests to the server.

If you explicitly do not want your connection to stay alive, set the Connection header to "close":

conn.setRequestProperty( "Connection","close" );

To keep the connection alive, you must set a valid Content-Length header every time you send some data. This must be set to the size, in bytes, of the data you are about to send:

byte data[] = new byte[55];
// Fill in the byte array with data here
c.setRequestProperty("Content-Length",data.length);

As long as the game client and game server deals with valid lengths of data, and as long as both parties don't issue the close() command, the client and server can communicate freely on this By default, HTTP connections are kept alive This means that the same connection will be used for multiple requests. connection.

Session Tracking

Although keeping an By default, HTTP connections are kept alive This means that the same connection will be used for multiple requests. HTTP connection alive is well and good, it is usually cleaner and easier for a server to close out a connection after every request.

When you surf between several Web pages on a big site, a browser cookie keeps track of who you are and enables the Web server to track a given session. However, MIDP doesn't support cookies.

Rather, the typical method of keeping track of sessions is by sending some sort of identification along with every request. This ID can be part of the message itself, sent in a special request header, or attached to the URL, as seen here:

http://www.testgame.com/servlet/GarageServlet?uname=fox

In our sample game, the MIDlet will tack on a username parameter to every message. This lets the server keep track of sessions.

The Messages

Given our game design parameters, Table 20.1 shows the commands and applicable parameters that will need to be handled.

Table 20.1. Necessary Game Parameters

Uname Takes the username as a parameter. Must be sent along with every message.
Action

login—. Logs in to the server. Sends down a list of items and list of other users.

logout—. Logs out and notifies you if anything has been sold.

buy—. Buy a particular item.

sell—. Sell a particular item.

checkin—. Just check in to the server to see how things are progressing.

Item When sent along with buy, just include the name of the item you want.
Sell When sent along with sell, you must include the item's name, description, and price, in the format ItemName!A Neat Item!59.

Weaknesses

Although the preceding design is functional, it has a lot of weaknesses. Here are just a few:

  1. There's no check to be sure two users aren't using the same username. Eventually, the system should include usernames and passwords, and only allow one person to use a particular account name.

  2. The amount of money you have and your car's items should be stored on the server in a database. Otherwise, it's easy for a hacker to pretend to have limitless amounts of money.

  3. If items were in a database, then the store could be persistent. You wouldn't have to remain logged in to sell your items. Rather, the system could sell items for you and notify you via e-mail when a purchase succeeds.

  4. All buy and sell operations should be discrete transactions—that is, a database should “lock” each object whenever a deal is made, to ensure that only one person is buying or selling it at a time.

  5. There's no real interaction. It would be nice to have some chat or other social functionality so people can express themselves, and allow the player to get an idea of the other players' personalities.

The Client Side

It then sends this number to a server. Note that for the sake of testing, it's a good idea to set the server to localhost—that way you can run the server and the client from the comfort of your desktop machine. Eventually, however, you will want to run the servlet on a live Web site and your MIDlet should connect to that URL. Different application servers allow you to set up servlets in different ways. A typical servlet setup will look like this: http://localhost:8080/servlet/GarageServlet.

Note that we created a ServerCallback interface with one method:

public void serverResponse( String response);

Every major game message is given its own class, each implementing ServerCallback. Each message connects to the server and passes in itself as the callback. That way, when the servlet issues a response, it can be handled by the appropriate class.

The full code listing is as follows:

import java.util.*;
import java.io.*;
import javax.microedition.io.*;
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
public class GarageClient extends MIDlet implements CommandListener
{
  private Display display;
  private Form loginform;
  private Form mainform;
  private Form detailform;
  private Form sellform;
  private Form sellform2;
  private Form itemsoldform;

  // Hard-coded for now. I have 200 dollars.
  public int mybalance = 200;
  // For now, just hard code the item data
  // TODO: Grab this from the database
  public String[] myitemname = {"Turbo Boost","Oil Slick","Wide Tires"};
  public String[] myitemdescript = {"Add 2 To Speed","Releases Oil On Track","Add 1 To
The Client Side Control"};

  // commands
  static final Command LOGIN = new Command("Log In",Command.SCREEN, 1);
  static final Command EXIT = new Command("Exit",Command.EXIT, 2);

  static final Command BUY = new Command("Buy",Command.SCREEN, 1);
  static final Command SELL = new Command("Sell",Command.SCREEN, 1);
  static final Command LOGOUT = new Command("Log Out",Command.SCREEN, 2);
  static final Command REFRESH = new Command("Refresh",Command.SCREEN, 2);

  static final Command BUYIT = new Command("Buy It",Command.SCREEN, 1);
  static final Command SELLIT = new Command("Sell It",Command.SCREEN, 1);
  static final Command BACK = new Command("Back",Command.BACK, 2);

  private TextField usernamefield;
  private TextField pricefield;
  private ChoiceGroup itemgroup;
  String username;
  private Vector itemvector;
  private Vector solditemvector;
  private int price;
  private String usershere;

  Timer timer;
  // Point to the servlet here...
  String url = "http://localhost:8080/servlet/GarageServlet";

  public GarageClient()
  {
    display = Display.getDisplay(this);
  }

  public void startApp()
  {
    loginform = new Form("The Garage");
    loginform.append("Log In Now");
    usernamefield = new TextField("Username:", "", 10, TextField.ANY);
    loginform.append(usernamefield);
    loginform.addCommand(LOGIN);
    loginform.addCommand(EXIT);
    loginform.setCommandListener(this);

    display.setCurrent(loginform);
  }

  public void pauseApp()  {  }

  public void destroyApp(boolean unconditional) { }

  public void commandAction(Command c, Displayable s)
  {
    if (s instanceof Form)
    {
        Form obj = (Form) s;
        if (obj == loginform)
        {
            if (c == EXIT)
            {
                 notifyDestroyed();
            }
            else if (c == LOGIN)
            {
                username = usernamefield.getString();
                Login l = new Login();
            }
        }
        else if (obj == mainform)
        {
            if (c == BUY)
            buydetail(itemgroup.getSelectedIndex());
            else if (c == SELL)
                sell();
            else if (c == LOGOUT)
            {
                Logout lo = new Logout();
            }
            else if (c == REFRESH)
            {
        Login l = new Login();
            }
        }
        else if (obj == sellform || obj == detailform || obj == itemsoldform)
        {
            if (c == BACK)
                display.setCurrent(mainform);
            else if (c == BUYIT)
            {
                BuyIt bi = new BuyIt(itemgroup.getSelectedIndex());
            }
            else if (c == SELLIT)
        pickprice();
        }
        else if (obj == sellform2)
        {
            if (c == BACK)
                display.setCurrent(sellform);
            else if (c == SELLIT)
            {
                String price = pricefield.getString();
                int i = itemgroup.getSelectedIndex();

        // Be sure item wasn't already put up for sale
                if (myitemname[i].equals(""))
            return;

                // Marshal data for item in a String
                String theitem = myitemname[i]+"!"+myitemdescript[i]+"!"+price;
                // Remove the item from the local list of my items
                myitemname[i] = "";
                myitemdescript[i] = "";

                SellIt si = new SellIt(theitem);
            }
        }
    }
  }

  void CreateMainform()
  {
    mainform = new Form("Items For Sale");
    mainform.addCommand(BUY);
    mainform.addCommand(SELL);
    mainform.addCommand(LOGOUT);
    mainform.addCommand(REFRESH);
    mainform.setCommandListener(this);

    mainform.append("Also Here: "+usershere+"
");
    mainform.append("My Balance: $"+mybalance+"
");

    if (itemvector.size() == 0)
    {
      mainform.append("No Items For Sale");
      mainform.append("Come Back Later");

          // Remove the buy button
          mainform.removeCommand(BUY);
    }
    else
    {
        String items[] = new String[itemvector.size()];
        // Now parse out the item name
        for (int i=0; i < itemvector.size(); i++)
            items[i] = ItemName((String)itemvector.elementAt(i));

        itemgroup = new ChoiceGroup("Exclusive",ChoiceGroup. EXCLUSIVE,items,null);

        mainform.append(itemgroup);
    }
  }

  void CreateItemsoldform()
  {
    itemsoldform = new Form("Item Sold!");
    itemsoldform.addCommand(BACK);
    itemsoldform.setCommandListener(this);
  }


  class Login implements ServerCallback
  {

  Login()
  {
      ConnectServer("login",null,this);
  }

  public void serverResponse(String response)
  {
    if (CheckForError(response)) return;

    // Parse out the user list and items for sale

    int uindex = response.indexOf("users");
    int findex = response.indexOf("forsale");
    if (uindex == -1 || findex == -1)
    {
        ShowError("Bad User List or For Sale List Returned");
        return;
    }
    usershere = response.substring(6,findex-1);
    String forsale = response.substring(findex+8);

    // Now parse through the for sale list
    itemvector = new Vector();
    CreateItemVector(forsale,itemvector);
    boolean canbuy = true;

    CreateMainform();

    display.setCurrent(mainform);
  }
  }

  private void buydetail(int at)
  {
    detailform = new Form("Ready To Buy");
    detailform.addCommand(BUYIT);
    detailform.addCommand(BACK);
    detailform.setCommandListener(this);

    detailform.append(ItemName((String)itemvector.elementAt(at))+"
");
    
    // Show the description
    detailform.append(ItemDescription((String)itemvector.elementAt(at))+"
");
    
    // Show the price
    String iprice = ItemPrice((String)itemvector.elementAt(at));
    detailform.append("For $"+iprice+"
");
    try {
    price = Integer.parseInt(iprice);
    }
    catch (NumberFormatException nfe)
    {
        price = 0;
    }

    // Do we have enough money?
    if (price > mybalance)
    {
     detailform.append("You Can't Afford This!");
    detailform.removeCommand(BUYIT);
    }
    display.setCurrent(detailform);
  }

  class BuyIt implements ServerCallback
  {
  int index;

  BuyIt(int itemindex)
  {
    index = itemindex;
ConnectServer("buy",itemgroup.getString(itemindex),this);
  }

  public void serverResponse(String response)
  {
    if (CheckForError(response)) return;
    if (response.equals(""))
    {
    // Charge your account
        mybalance -= price;

        // TODO: Add item to your personal database

        // Remove item from local list
    itemvector.removeElementAt(index);

    CreateMainform();
        display.setCurrent(mainform);
    }
    else
        ShowError(response);
  }
  }

  private void sell()
  {
    sellform = new Form("Your Items");
    sellform.addCommand(SELLIT);
    sellform.addCommand(BACK);
    sellform.setCommandListener(this);

    itemgroup = new ChoiceGroup("Exclusive",ChoiceGroup. EXCLUSIVE,myitemname,null);
    sellform.append(itemgroup);
    display.setCurrent(sellform);
  }

  private void pickprice()
  {
    sellform2 = new Form("Choose Sale Price");
    sellform2.addCommand(SELLIT);
    sellform2.addCommand(BACK);

    sellform2.setCommandListener(this);

    sellform2.append(itemgroup.getString(itemgroup.getSelectedIndex())+"
");

    pricefield = new TextField("Price:", "", 3, TextField.NUMERIC);
    sellform2.append(pricefield);

    display.setCurrent(sellform2);
  }

  class SellIt implements ServerCallback
  {

  SellIt(String item)
  {
    ConnectServer("sell", item, this);
  }

  public void serverResponse(String response)
  {
    if (CheckForError(response)) return;

    // Begin polling the server for any updates
    if (timer == null)
    {
        timer = new Timer();
    CheckInTimer ci = new CheckInTimer();
    timer.schedule(ci,(long)10000,10000);
    }
    display.setCurrent(mainform);
  }
  }

  class Logout implements ServerCallback
  {

  Logout()
  {
    if (timer != null)
    timer.cancel();
    ConnectServer("logout",null,this);
  }
  public void serverResponse(String response)
  {
    if (CheckForError(response)) return;
    display.setCurrent(loginform);
  }
  }

  class CheckIn implements ServerCallback  {

  CheckIn()
  {
    ConnectServer("checkin",null,this);
  }

  public void serverResponse(String response)
  {
    if (CheckForError(response)) return;
    int rindex = response.indexOf("sold");
    if (rindex != -1)
    {
        String items = response.substring(rindex+5);
        if (!items.equals(""))
        {
       CreateItemsoldform();
        solditemvector = new Vector();
           CreateItemVector(items,solditemvector);

           // Now parse out the item name
           for (int i=0; i < solditemvector.size(); i++)
           {
            String itemname = ItemName((String)solditemvector.elementAt(i));
            String itemprice = ItemPrice((String)solditemvector.elementAt(i));
              int price = 0;
              try {
                price = Integer.parseInt(itemprice);
              }
              catch (NumberFormatException nfe) { }

              // TODO: Remove the item from the database
          itemsoldform.append(itemname+" sold for $"+itemprice);

              // Add money to your balance
              mybalance += price;
          CreateMainform();
          display.setCurrent(itemsoldform);
           }
        }
    }
  }
  }

  void CreateItemVector(String items,Vector iv)
  {
      // Parse through the list of items
      int start = 0;
      int end = 0;
      String item = "";
      end = items.indexOf(','),

      while (end != -1)
      {
       item = items.substring(start,end);
       iv.addElement(item);
       start = end+1;
       end = items.indexOf(',',start);
      }
      item = items.substring(start);
      if (!item.equals(""))
    iv.addElement(item);
  }

  private boolean CheckForError(String response)
  {
     System.out.println("RESP:"+response);
     int ecode = response.indexOf("error");
     if (ecode != -1)
     {
    String therror = response.substring(ecode+6);
    ShowError(therror);
    return true;
     }
     return false;
  }

  private void ShowError(String err)
  {
    Alert errorAlert = new Alert("Alert",err,null, AlertType.ERROR);
    errorAlert.setTimeout(Alert.FOREVER);
    display.setCurrent(errorAlert);
  }

  private String ItemName(String item)
  {
    int end = item.indexOf('!'),
    if (end == -1)
    {
        ShowError("Bad Item Format");
        return "";
    }
    return item.substring(0,end);
    // Horse!mean!669
  }

  private String ItemDescription(String item)
  {
    int start = item.indexOf('!'),
    int end = item.indexOf('!',start+1);
    if (end == -1 || start == -1)
    {
        ShowError("Bad Item Format");
        return "";
    }
    return item.substring(start+1,end);
  }

  private String ItemPrice(String item)
  {
    int start = item.indexOf('!'),
    int end = item.indexOf('!',start+1);
    if (end == -1 || start == -1)
    {
        ShowError("Bad Item Format");
        return "";
    }
    return item.substring(end+1);
  }
  void ConnectServer(String action,String item,ServerCallback callback)
  {
      ConnectNow cn = new ConnectNow(action,item,callback);
      cn.start();
  }

  public interface ServerCallback
  {
      public void serverResponse( String response);
  }

  class ConnectNow implements Runnable
  {
    String action;
    String item;
    ServerCallback callback;

    ConnectNow( String a,String i,ServerCallback c)
    {
      action = a;
      item = i;
      callback = c;
    }

    public void run()
    {
      HttpConnection c = null;
      InputStream is = null;
      DataOutputStream os = null;
      StringBuffer b = new StringBuffer();

      if (item == null)
          item = "";

      try
      {
        c = (HttpConnection)Connector.open(url);
        c.setRequestMethod(HttpConnection.POST);
        c.setRequestProperty("User-Agent","Profile/MIDP-1.0 Configuration/CLDC-1.0");
        c.setRequestProperty("Content-Language", "en-US");
        os = new DataOutputStream(c.openOutputStream());
        os.writeUTF(username);
        os.writeUTF(action);
        os.writeUTF(item);
    System.out.println(username+","+action+","+item);

        int rc = c.getResponseCode();
        if( rc == HttpConnection.HTTP_OK )
        {
          is = c.openDataInputStream();
          int ch;
          while ((ch = is.read()) != -1)
              b.append((char) ch);
        }
        else
        {
         System.out.println("Response Code: "+rc);
           ShowError("Bad Server Response!");
        }
      }
      catch (Exception e)
      {
          ShowError("Problem Connecting to Network");
      }
      finally
      {
        try {
        if (is != null)
          is.close();
        if (c != null)
          c.close();
        if (os != null)
          os.close();
        }
        catch (Exception e)
        {
          ShowError("Problem Closing Network Connection");
        }
      }
      if (b != null)
          callback.serverResponse(b.toString());
      else
          callback.serverResponse(null);
    }

    void start()
    {
       Thread t = new Thread( this );
       try
       {
          t.start();
       }
       catch( Exception e )
       {
           ShowError(e.toString());
       }
    }
  }

  class CheckInTimer extends TimerTask
  {
    CheckInTimer() { }

    public void run()
    {
      try {
          CheckIn ci = new CheckIn();
      }
      catch (Exception ex) { }
    }
  }
}

The Server Side

The Garage servlet itself is pretty simple—once kicked off, it remains running. It listens for new connections, handles commands, and returns data.

The Game Data

Encapsulating the game data itself in Java classes is an exceptionally easy way to keep track of things. Eventually, these classes could be turned into Enterprise JavaBeans, enabling all the data to be stored permanently in a database.

Basically, we have two objects we care about: CarItem and CarUser.

For the sake of this simplified demo, CarUser merely contains one item—the user's name.

We'll also create an equals method to help the servlet figure out whether two users are the same:

public class CarUser {
    public String name;

    CarUser (String n) {
        name = n;
    }

    public boolean equals(CarUser cu) {
        return (cu.name.equals(name));
    }
}

Meanwhile, the CarItem class isn't that much more complicated. It just holds the items that are for sale, their description, and the sale price:

public class CarItem {
    public String name;
    public String description;
    public int cost;
    public String ownername;

    CarItem (String n,String d,int c,String o) {
        name = n;
        description = d;
        cost = c;
        ownername = o;
    }
}

Note that eventually you could use CarItem to keep track of things on both the client side as well as the server side. You could also create special classes that extend CarItem—for example, the Armor class, Wheel class, Weapon class, Booster class, or Engine class.

Let's just concentrate on one of these, the Weapon class. The class is relatively simple. It basically just acts as a container to hold several useful variables.

It could, in theory, look like the following code fragment, containing the amount of damage the weapon extracts, the amount of ammunition left, and whether the weapon is situated on the front, side, or back of the car.

public class Weapon extends CarItem
{
  static public final int FRONT = 0;
  static public final int REAR = 1;
  static public final int LEFT = 2;
  static public final int RIGHT = 3;

  public int weapon;
  public int damagepoints;
  public int ammunition;
  public int location;

  Weapon(String name,String descrip,int cost,String ownname,int points,int ammo,int loc)
  {
    super(name,descrip,cost,ownname);
    damagepoints = points;
    ammunition = ammo;
    location = loc;
  }
}

To create a flamethrower, you would just use

Weapon ft = new Weapon("flamethrower","Burns Vehicles In Front", 100,myname,25,16,Weapon
The Game Data.FRONT);

The Servlet

Running on the localhost machine is the GarageServlet servlet, which simply waits for HTTP messages.

The full code is as follows:

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class GarageServlet extends HttpServlet {
// Store the users, items for sale, and items sold
private static Vector users = new Vector();
private static Vector forSale = new Vector();
private static Vector sold = new Vector();

    public GarageServlet() { }

    public void doPost(HttpServletRequest request,HttpServletResponse response)
      throws ServletException,IOException
    {
      // Get the input stream
      ServletInputStream in = request.getInputStream();
      DataInputStream din = new DataInputStream(in);

    try {
      String name = din.readUTF();
      String action = din.readUTF();
      String item = din.readUTF();
      din.close();
    SendOutput(name,action,item,response);
    }
    catch (Exception e)
    {
      }
    }

    public void doGet(HttpServletRequest request,HttpServletResponse response)
      throws ServletException,IOException
    {
      // get the input through the URL
      String name = request.getParameter("uname");
         String action = request.getParameter("action");
         String item = request.getParameter("item");
    SendOutput(name,action,item,response);
    }

    private synchronized void SendOutput(String name,String action,String item
The Servlet,HttpServletResponse response) throws IOException
    {
    PrintWriter out = new PrintWriter (response.getOutputStream(), true);
             response.setStatus( response.SC_OK );
      if (name == null)
          return;

      if (action.equals("login"))
      {
            // Be sure we aren't already logged in
            CarUser cu = FindUser(name);
            if (cu == null)
            {
                cu = new CarUser(name);
                  // add to user list
                  users.addElement(cu);
              }

            // send down list of users
            SendUsers(out);
            out.print("&");

            // send down list of items for sale
            SendItemsForSale(out);

            out.close();
      }
      // Otherwise, be sure this user is on the system
      CarUser thisuser = FindUser(name);
      if (thisuser == null)
      {
         SendError(out,"Not Logged In");
         return;
      }
      if (action.equals("logout"))
      {
            // remove this user's items from the for sale list
            CarItem ci = FindItemOwnedBy(name);
            while (ci != null)
            {
                forSale.removeElement(ci);
                ci = FindItemOwnedBy(name);
            }

              // remove user from list
              users.removeElement(thisuser);
              NotifyOfItemsSold(out,name);
      }
      else if (action.equals("buy"))
      {

            if (item == null)
            {
                SendError(out,"No Item Specified");
                return;
            }

            // Be sure requested item name is on the list
            CarItem ci = FindItem(item);
            if (ci != null)
            {
                // Remove item from the for sale list
                forSale.removeElement(ci);

                // Add item to sold list
                sold.addElement(ci);
            }
            else
            {
                SendError(out,"Item Not Found");
                return;
            }
      }
      else if (action.equals("sell"))
      {
            if (item == null)
            {
                SendError(out,"No Item Specified");
                return;
            }

            // Parse out the name, descrip, and cost
           String iname = "";
           String descrip = "";
           int cost = 0;
           StringTokenizer st = new StringTokenizer(item,"!");
           try {
           iname = st.nextToken();
           descrip = st.nextToken();
           try {
            cost = Integer.parseInt(st.nextToken());
           }
           catch (NumberFormatException nfe) {
                SendError(out,"Bad Price");
                return;
           }
           }
           catch (NoSuchElementException  nse) {
                SendError(out,"Bad Item Format");
                return;
           }
           // Create the CarItem object
           CarItem ci = new CarItem(iname,descrip,cost,name);
           // Add item to for sale list
           forSale.addElement(ci);
      }
      else if (action.equals("checkin"))
      {
              NotifyOfItemsSold(out,name);
      }
      else
        SendError(out,"Illegal Action");
      out.close();
    }

    private static void SendError(PrintWriter out,String error)
    {
        out.print("error="+error);
        out.close();
    }

    private static void SendUsers(PrintWriter out)
    {
      out.print("users=");
      for (int i=0; i < users.size(); i++)
      {
          CarUser cu = (CarUser)users.elementAt(i);
          out.print(cu.name);
          if (i != users.size()-1)
              out.print(",");
      }
      }

    private static void SendItemsForSale(PrintWriter out)
    {
      out.print("forsale=");
      for (int i=0; i < forSale.size(); i++)
      {
          CarItem ci = (CarItem)forSale.elementAt(i);
          out.print(ci.name+"!"+ci.description+"!"+ci.cost);
          if (i != forSale.size()-1)
              out.print(",");
      }
      }

    private static void NotifyOfItemsSold(PrintWriter out,String owner)
    {
      out.print("sold=");
      for (int i=0; i < sold.size(); i++)
      {
          CarItem ci = (CarItem)sold.elementAt(i);
          if (ci.ownername.equals(owner))
          {
                // Remove the item from the sold list
                sold.removeElement(ci);
                 out.print(ci.name+"!!"+ci.cost);
              if (i != sold.size()-1)
                  out.print(",");
        }
      }
      }

      private CarItem FindItem(String name)
      {
      for (int i=0; i < forSale.size(); i++)
      {
          CarItem ci = (CarItem)forSale.elementAt(i);
          if (ci.name.equals(name))
              return ci;
        }
        // Item not in list
        return null;
      }

      private CarItem FindItemOwnedBy(String name)
      {
      for (int i=0; i < forSale.size(); i++)
      {
          CarItem ci = (CarItem)forSale.elementAt(i);
          if (ci.ownername.equals(name))
              return ci;
        }
        // Item not in list
        return null;
      }

      private CarUser FindUser(String name)
      {
      for (int i=0; i < users.size(); i++)
      {
          CarUser cu = (CarUser)users.elementAt(i);
          if (cu.name.equals(name))
              return cu;
        }
        // Item not in list
        return null;
      }
}

Playing the Game

First you log into the Garage, as shown in Figure 20.2. You then see a list of who's there (Figure 20.3) and what's for sale (Figure 20.4). You can then choose an item that you're interested in. Detailed information about its abilities and enhancements will be shown to you, as in Figure 20.5. You now have the choice of buying it or bailing out. If you buy it, the price is deducted from your account and you now own it.

Logging in.

Figure 20.2. Logging in.

Who's here…

Figure 20.3. Who's here…

…and what's for multiplayer gamesplayingcar racing gameplayingdesigncar racing gameplayingplayingracing car gamesale.

Figure 20.4. …and what's for sale.

An item's detail.

Figure 20.5. An item's detail.

You might also, of course, decide to sell your own goodies to make some extra money. To do so, just hit the Sell button. A list of all your items appears, as can be seen in Figure 20.6.

A list of your items.

Figure 20.6. A list of your items.

You simply need to set your sale price, as shown in Figure 20.7. You can set this as high as you want, but if you make your item too costly, you will most likely not get any buyers.

Choosing a sale price.

Figure 20.7. Choosing a sale price.

As you wait, your MIDlet will automatically poll the server every few seconds for any updated info. If somebody decides to buy your item, you are informed as shown in Figure 20.8.

Your itemmultiplayer gamesplayingcar racing gameplayingdesigncar racing gameplayingplayingracing car game has been sold!

Figure 20.8. Your item has been sold!

Summary

And so there you have it—a full-featured (more or less) multiplayer game component!

In Chapter 19, we created a local data store with game information, such as how much money you have, which objects you own, and so on. Obviously, it makes lots of sense to tie the Garage client to this same data store, so that everything you buy and sell is persistent from one game to the next.

In Chapter 24, “Micro Racer: Putting It All Together” we will tie together the data store, the multiplayer buy-and-sell networking component, as well as all the action components. In the end, we'll have a complete, competitive action game with a built-in online community.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.173.53