Multiplayer I m a circle --A Sample Network Game

Multiplayer "I'm a circle!" —A Sample Network Game

Before we start, the aim of this example is not to make a cool network game but rather to show you how to create a solid foundation for one. The aim of this example is to have a server that will run in the console window, which clients can connect to and move their position around by clicking where they want to go to with the mouse. In addition, the player's name will be displayed to the right of them (the player will just be represented by a circle).

By the end of this example, you will have a great network framework, which you can easily implement into your own games just as easily as you can now handle input and graphics in your games.

Let's start by looking at the server, as it is the simpler of the two applications that we need (the server and the client). Note we need several source files for both, but we will look at them one at a time (however, all the code will be shown here, just one file at a time). As a reference, the server application consists of the following four .java source files: SampleServer (main class), ClientHandler, Player, and Protocol. Let's take a look at the source code.

Creating the Server

Let's start by looking at the complete listing of the main class, which we have called SampleServer. The complete code listing can be seen here:

Code Listing 17-5: SampleServer.java

start example
import java.net.*; import java.io.*; import java.util.*;     public class SampleServer implements Runnable {     public SampleServer(int port)     {         // create the server socket...         try         {             serverSocket = new ServerSocket(port);         }         catch(IOException e)         {             System.out.println("-> Could not create Server on port                 "+port);             System.exit(1);         }                  System.out.println("-> Server created on port "+port);     }          public void run()     {         while(true)         {             // wait for a client connection...             try             {                 System.out.println("-> Waiting for client                     connections...");                 new ClientHandler(serverSocket.accept());             }             catch(IOException e)             {                 System.out.println("-> Error accepting client                     connection: "+e);             }         }     }          public static void main(String args[])     {             // create the server...         SampleServer server = new SampleServer(9000);         new Thread(server).start();     }          private ServerSocket serverSocket; }
end example

Starting in the main method, we first create a new instance of our SampleServer class, passing in 9000, which represents the port on which we wish to create the server. In the constructor of SampleServer, we first attempt to create the server by creating a new ServerSocket, passing in the port value. The code to do this can be seen here:

try {     serverSocket = new ServerSocket(port); } catch(IOException e) {     System.out.println("-> Could not create Server on port "+port);     System.exit(1); } 

Notice how we catch the IOException in case the server could not be created (a possible cause of this would be a server already running on the specified port). If this occurs, we simply exit the application by calling the System.exit method.

Next we jump back to the main method and create a new thread to run the server on by creating a new Thread object, passing in our sampleServer object that we just created. The run method for the server is then invoked when we create and start the thread.

Execution now goes into the run method, so let's look at this now. This is simple, actually. First, we create an infinite while loop, and then we call the accept method of our serverSocket object that we created in the constructor. Note that this method blocks, so when we call it, it will wait until a client connects to the server; and when this occurs, it will return a Socket object that will contain the two streams associated with the client. The returned Socket object is then passed when constructing a new ClientHandler object. So when we get a client connection, it will create a new ClientHandler object (we will look at this class in a second), passing in the Socket object associated with that client.

Let's now look at the complete source code listing for the ClientHandler class.

Code Listing 17-6: ClientHandler.java

start example
import java.net.*; import java.io.*; import java.util.*; import java.awt.*;     public class ClientHandler implements Protocol {     public ClientHandler(Socket socket)     {         try         {             this.socket = socket;                          DataInputStream in = new DataInputStream                 (socket.getInputStream());             DataOutputStream out = new DataOutputStream                 (socket.getOutputStream());                          incomingMessageHandler = new IncomingMessageHandler(in);             outgoingMessageHandler = new OutgoingMessageHandler(out);                          connected = true;                          Random rand = new Random();                          player = new Player(rand.nextInt(640), rand.nextInt(480),                 rand.nextInt(Player.colors.length), uniqueIdCount);              uniqueIdCount++;                                                synchronized(clientList)             {                 clientList.add(this);             }                                      sendMessage(MSG_INIT_PLAYER+"|"+player.x+"|"+player.y+"|"                 +player.colId+"|"+player.uniqueId);         }         catch(IOException e)         {             System.out.println("Unable to connect: "+e);         }     }               public class IncomingMessageHandler implements Runnable     {         public IncomingMessageHandler(DataInputStream in)         {             inStream = in;             receiver = new Thread(this);             receiver.start();         }              public void run()         {             Thread thisThread = Thread.currentThread();             while(receiver==thisThread)             {                 try                 {                     String message = inStream.readUTF();                     handleMessage(message);                 }                 catch(IOException e)                 {                     disconnect();                 }             }         }                  public void destroy()         {             receiver = null;         }                  Thread receiver;         private DataInputStream inStream;     }               public class OutgoingMessageHandler implements Runnable     {         public OutgoingMessageHandler(DataOutputStream out)         {             outStream = out;             messageList = new LinkedList();                          sender = new Thread(this);             sender.start();         }                  public void addMessage(String message)         {             synchronized(messageList)             {                 messageList.add(message);                 messageList.notify();             }         }                          public void run()         {             String message;                          Thread thisThread = Thread.currentThread();             while(sender==thisThread)             {                 synchronized(messageList)                 {                     if(messageList.isEmpty() && sender!=null)                     {                         try                         {                             messageList.wait();                         }                         catch(InterruptedException e) { }                     }                 }                                  while(messageList.size()>0)                 {                     synchronized(messageList)                     {                         message = (String)messageList.removeFirst();                     }                                          try                     {                         outStream.writeUTF(message);                     }                     catch(IOException e)                     {                         disconnect();                     }                 }             }                 }                  public void destroy()         {             sender = null;                          synchronized(messageList)             {                 messageList.notify();                  // wake up if stuck in waiting stage             }         }                  Thread sender;         LinkedList messageList;         DataOutputStream outStream;     }               public synchronized void disconnect()     {         if(connected)         {             synchronized(clientList)             {                 clientList.remove(this);             }             broadcast(MSG_REMOVE_PLAYER+"|"+player.uniqueId);                                       connected = false;                          incomingMessageHandler.destroy();             outgoingMessageHandler.destroy();                      try             {                 socket.close();             }             catch(Exception e) {}             socket = null;         }                  System.out.println("-> Client Disconnected");     }               public static void broadcast(String message)     {         synchronized(clientList)         {             ClientHandler client;             for(int i=0; i<clientList.size(); i++)             {                 client = (ClientHandler)clientList.get(i);                 client.sendMessage(message);             }         }     }               public void broadcastFromClient(String message)     {         synchronized(clientList)         {             ClientHandler client;                 for(int i=0; i<clientList.size(); i++)             {                 client = (ClientHandler)clientList.get(i);                 if(client!=this)                     client.sendMessage(message);             }         }     }                 public void sendMessage(String message)     {         outgoingMessageHandler.addMessage(message);     }               public void handleMessage(String message)     {         StringTokenizer st = new StringTokenizer(message, "|");         int type = Integer.parseInt(st.nextToken());                  switch(type)         {             case MSG_SET_NAME:             {                 player.name = st.nextToken();                 sendMessage(MSG_SET_NAME+"|"+player.name);                 broadcastFromClient(MSG_ADD_NEW_PLAYER+"|"+player.x+                    "|"+player.y+"|"+player.colId+"|"+player.uniqueId                    +"|"+player.name);                                  Player p;                 //  tell this player about everyone else                                  synchronized(clientList)                 {                     for(int i=0; i<clientList.size(); i++)                     {                         p = ((ClientHandler)clientList.get(i))                             .player;                         if(player != p)                         {                             sendMessage(MSG_ADD_NEW_PLAYER+"|"+p.x                                 +"|"+p.y+"|"+p.colId+"|"+p.uniqueId+                                 "|"+p.name);                         }                     }                 }                 break;             }                          case MSG_MOVE_POSITION:             {                 player.x = Integer.parseInt(st.nextToken());                 player.y = Integer.parseInt(st.nextToken());                              broadcast(MSG_MOVE_POSITION+"|"+player.uniqueId+"|"                     +player.x+"|"+player.y);                 break;             }         }     }         private Socket socket;         private IncomingMessageHandler incomingMessageHandler;     private OutgoingMessageHandler outgoingMessageHandler;          public boolean connected;         public static ArrayList clientList = new ArrayList();          public Player player;     public static int uniqueIdCount; } 
end example

Yes, it is slightly larger but nothing to fear. We'll start by looking at the constructor, which, as you noted a minute ago, is called from the SampleServer's run method when a client connects.

First, we store a copy of the socket reference, which was passed into the constructor as an instance member of the ClientHandler class. This can be seen here:

this.socket = socket;

Next, we obtain the input and output streams from the Socket object by calling the getInputStream() and getOutputStream() methods and passing them into the constructors of DataInputStream and DataOutputStream objects, respectively. This can be seen here:

DataInputStream in = new DataInputStream(socket.getInputStream()); DataOutputStream out = new DataOutputStream(socket      .getOutputStream());

Next we create IncomingMessageHandler and OutgoingMessageHandler objects by passing in the input and output streams, resectively. We will look at these inner classes and their purpose in a moment. Once these are created, we then set a Boolean variable called connected to true, which simply notes that this client is now connected to the server.

Next we have some specific code for this circle example, which creates a new Player object when a client connects and assigns it some random values (such as the screen position and color). In addition, it assigns the client (player) a unique ID value, which can be used to reference the player at a later time. Here is the code we use in the constructor to create the Player object.

Random rand = new Random();               player = new Player(rand.nextInt(640), rand.nextInt(480),      rand.nextInt(Player.colors.length), uniqueIdCount);   uniqueIdCount++;

The player class is a simple data structure for storing data to make each player individual from any other. Each instance of ClientHandler will have its own Player object, as each ClientHandler instance is a client itself. Here is the source code for the player class:

Code Listing 17-7: Player.java

start example
import java.awt.*;     public class Player {     public Player(int x, int y, int colId, int uniqueId)     {         this.x = x;         this.y = y;         this.colId = colId;         this.uniqueId = uniqueId;     }         public int x, y;     public String name;     public int colId;     public int uniqueId;         public static final Color[] colors = {Color.red, Color.green,         Color.blue, Color.yellow, Color.magenta}; }
end example

Note that we will be reusing this player class when we come to creating the client. Once the Player object is created in the ClientHandler constructor, we then add this new client (ClientHandler object) to a clientList, which is simply a static ArrayList object containing a list of all the ClientHandler objects created, hence all of the clients connected to the server. This can be seen here:

synchronized(clientList) {     clientList.add(this); }

Note how we have synchronized adding to the clientList object. This is because we may be performing other operations, such as looping through it or removing other clients, as we do later in the code. So we want to keep all these operations synchronized to avoid any problems. We will see later why we are storing all the clients in a list.

The final part of the constructor is where we send a message to the client, which tells them information that the server has initialized for them (i.e., the x, y position, its color, and the player's unique ID). This is done by means of the sendMessage method, which we will look at in a moment. Note how we have used MSG_INIT_PLAYER as the type of message that we are sending. This is simply a static final int value that we have defined in a Protocol interface, which this class (ClientHandler) implements. Let's look at the Protocol interface in full now.

Code Listing 17-8: Protocol.java

start example
public interface Protocol {     public static final int MSG_MOVE_POSITION = 0;     public static final int MSG_SET_NAME = 1;     public static final int MSG_INIT_PLAYER = 2;     public static final int MSG_ADD_NEW_PLAYER = 3;     public static final int MSG_REMOVE_PLAYER = 4; } 
end example

We will see all of these messages getting used as we progress. Also, note how we are sending the data in the message. We are going to be sending strings across the network; however, we are separating the data within the string with the | character. Note that we are sending string values for simplicity purposes. As we will see, when we retrieve a message, we can use a StringTokenizer to first get the type of message (the first token converted to an integer value) and then use this to know which tokens the rest of the message contains.

The message that we are going to be sending to the client can be seen here:

sendMessage(MSG_INIT_PLAYER+"|"+player.x+"|"+player.y+"|"     +player.colId+"|"+player.uniqueId);

The first token is the message type (which is MSG_INIT_PLAYER), and then we send the x position, followed by the y position, the color ID, and finally the unique ID of the player.

Let's have a look at how we are sending these messages. If you remember in the constructor, we created an outgoingMessageHandler, which we will use now to send our messages. In the sendMessage method, all we actually do is add the message to the outgoingMessageHandler. This can be seen here:

public void sendMessage(String message) {     outgoingMessageHandler.addMessage(message); }

Now let's look at the constructor of the OutgoingMessageHandler, which we called in the constructor of our ClientHandler class, passing in the socket's output stream. Here is the complete constructor for the OutgoingMessageHandler (note that the OutgoingMessageHandler is a nested class of our ClientHandler).

public OutgoingMessageHandler(DataOutputStream out) {     outStream = out;     messageList = new LinkedList();                  sender = new Thread(this);     sender.start(); }

All we do here is store a reference to the output stream passed in called out in an instance member called outStream. Then we create a LinkedList called messageList, which will store a list of messages that are waiting to be sent. We then create a thread that will be used to actually send the messages and start it.

Next in this class we have defined a method called addMessage (which if you remember, we called from the sendMessage method in our ClientHandler). Here is the complete code for this addMessage method:

public void addMessage(String message) {     synchronized(messageList)     {         messageList.add(message);         messageList.notify();     } }

As you can see, this method takes in the message to be sent as a parameter and then adds the message to the messageList (which contains a list of outgoing messages waiting to be sent). Then we call the notify method, which will wake up the sender thread that we created in the constructor, which goes to sleep when there are no messages to be sent. We synchronize on the messageList object here, as we also synchronize on this object when removing messages from the list in the sender thread and when going to sleep in the sender thread.

The next method we have is the actual run method, which first goes into a loop and then executes the following block of code:

synchronized(messageList) {     if(messageList.isEmpty() && sender!=null)     {         try         {             messageList.wait();         }         catch(InterruptedException e) { }     } }

This code first synchronizes on the messageList (so no messages can be added when the execution enters this block of code); if you notice before the actual adding of messages, we also synchronized on the messageList. So once we enter this synchronized block, we then check to see if the messageList is empty (i.e., there are no messages waiting to be sent) and the thread is still running (i.e., sender != null). The sender!=null is for disconnection purposes, as destroy, which we will see later, defines some code to support exiting from this thread safely in this scenario. If there are no messages to be sent, we can put the thread to sleep by calling the wait method, which will also release the monitor on the messageList object so messages can be added again. If you remember from before, when a message is added, it calls the notify method of the messageList, so the thread will be signaled to wake up again from the wait call.

When the thread wakes up, it will then send all the messages that are waiting to be sent. So we create a while loop, which ensures there is at least one message to be sent—this can be seen here:

while(messageList.size()>0) {

Then we remove the first message from the messageList (as new messages are added to the end so this gives us a first in, first out queue). This can be seen here.

synchronized(messageList) {     message = (String)messageList.removeFirst(); } 

Notice also that we have synchronized on the messageList object again here, as we do not want any other operation occurring that requires exclusive access while we are removing and obtaining the message from it, hence adding to it.

We then have the message to be sent in a String object called message. Once we have this, we can then send the message by calling the writeUTF method of the DataOutputStream, passing in the String object to be sent. This can be seen here:

try {     outStream.writeUTF(message); } catch(IOException e) {     disconnect(); }

Notice here how we also call the disconnect method of the ClientHandler class if an IOException occurred when trying to write to this output stream, as this would signify that a connection to the client no longer existed. We will look at the disconnect method soon. However, for now let's look at the final method declared in the OutgoingMessageHandler class, the destroy method:

public void destroy() {     sender = null;          synchronized(messageList)     {         messageList.notify(); // wake up if stuck in waiting stage     } }

The purpose of this method is to simply stop the thread from running (by setting sender to null) and wake up the thread if it is sleeping. This method is called from the disconnect method in the ClientHandler class, which we will look at shortly. Note the order of actions means that we will still terminate the thread's execution if the run method is currently executing at the point just before entering the synchronized block where it will go to sleep, as we saw before. With this code, the run method is guaranteed to exit, as the sender reference is set to null, and then the thread is woken up if it is asleep. In the run method, you'll see that it won't go to sleep if sender equals null. This is all so that we don't notify just before going to sleep at the start of the loop in the run method, where we could have gone to sleep after calling notify.

As well as the sendMessage method, we have also created two other useful methods in the ClientHandler class for sending messages; these are broadcast and broadcastFromClient. The broadcast method simply loops through the clientList (remember that we add each client which connects to this list), calling the sendMessage method of each of the clients, so a message is sent to everyone connected to the server. The broadcast method can be seen here:

public static void broadcast(String message) {     synchronized(clientList)     {         ClientHandler client;         for(int i=0; i<clientList.size(); i++)         {             client = (ClientHandler)clientList.get(i);             client.sendMessage(message);         }     } }

Notice here that we synchronize on the clientList object, so no clients can be added or removed while we are adding our messages to their respective OutgoingMessageHandler objects. In case you weren't aware of this fact, each client has its own OutgoingMessageHandler and IncomingMessageHandler.

The other method, broadcastFromClient, is pretty much the same, but it sends to all the clients except the client that the method is being called from. This can be seen here:

public void broadcastFromClient(String message) {     synchronized(clientList)     {             ClientHandler client;             for(int i=0; i<clientList.size(); i++)         {             client = (ClientHandler)clientList.get(i);             if(client!=this)                 client.sendMessage(message);         }     } }

The only difference in this method is that we have the extra if check to ensure that the client in the list that we are about to send to does not equal this client object. This is useful when we don't need to tell the client who sent the data about an update, as it already knows about this data because it sent it.

Now that we have looked at the sending of messages, let's look at how the IncomingMessageHandler class works. This class is actually a lot simpler than the OutgoingMessageHandler, as we will see now. Let's first look at the constructor, which is called in the constructor of the ClientHandler class, where we pass in a DataInputStream object, which was created from the input stream of the socket. All we do in the constructor of the IncomingMessageHandler is copy the reference of the input stream passed in as in to an instance member called inStream. Then we create a thread called receiver and start it.

public IncomingMessageHandler(DataInputStream in) {     inStream = in;     receiver = new Thread(this);     receiver.start(); }

Let's look at the run method of this class now. All we do here is start a loop as we normally do and then call the readUTF method of the inStream, which is the input stream connected to the socket that was passed into the constructor. The readUTF method blocks until there is a string ready to be read from the stream; then it reads it in and returns it. We then assign the input String object to the message reference. Then we pass this message to a method defined in the ClientHandler class called handleMessage, which we will look at in a moment. Finally, we catch a possible IOException, which could occur if the connection is broken either by a client closing or a hardware failure (i.e., someone pulling out a network cable). If this does occur, as with the OutgoingMessageHandler, we simply call the disconnect method of the ClientHandler class (which again we will look at soon). The complete run method can be seen here:

public void run() {     Thread thisThread = Thread.currentThread();     while(receiver==thisThread)     {         try         {             String message = inStream.readUTF();             handleMessage(message);         }         catch(IOException e)         {             disconnect();         }     } }

Finally, in our IncomingMessageHandler, we have a destroy method, which simply stops the receiver thread from running by setting the receiver reference to null. We will see this being used in the disconnect method. Here is the short but sweet destroy method:

public void destroy() {     receiver = null; }

So when a message is received in the incomingMessageHandler thread, it is then passed to the handleMessage method of the ClientHandler class, which we are going to use to actually determine what the message is and react to it accordingly. So, in the handleMessage method, we first determine the type of message that has been received by using the following two lines of code:

StringTokenizer st = new StringTokenizer(message, "|"); int type = Integer.parseInt(st.nextToken());

The first line creates a StringTokenizer object using the message as the string to tokenize and the character | to denote the different tokens. Then we get the first token from the message, which is always going to be the type of message (as defined in the Protocol interface that we created earlier in this section). So we can then switch the message type and create a case for each different type of message that a client could receive.

The first message that we are going to make the server handle is when the player sets his or her name. We first need to create a case for this, as follows:

switch(type) {     case MSG_SET_NAME:     {

Then we obtain the player's name from the next token, which can be seen here:

player.name = st.nextToken();

Then we send a message back to the player to confirm that the name has been set using the following line of code, which will then update the player's name data in the client:

sendMessage(MSG_SET_NAME+"|"+player.name);

This may not make much sense now, but it should all come together when you read about how the client works.

We then broadcast the addition of a new player and all his details to everyone (except the current client that the player belongs to), so all the other connected clients can add this player to their programs, as we shall see later in the client. The line of code to do this can be seen here:

broadcastFromClient(MSG_ADD_NEW_PLAYER+"|"+player.x+"|"+player.y+"|"     +player.colId+"|"+player.uniqueId+"|"+player.name);

Finally, for this message, we need to send this client a list of all the players that are currently connected to the server, To do this, we need to cycle through our clientList and send each of the clients' player details to this client (without sending this player's details to him/herself). This is done with the following block of code:

Player p; //  tell this player about everyone else   synchronized(clientList) {     for(int i=0; i<clientList.size(); i++)     {         p = ((ClientHandler)clientList.get(i)).player;         if(player != p)         {         sendMessage(MSG_ADD_NEW_PLAYER+"|"+p.x+"|"+p.y+"|"             +p.colId+"|"+p.uniqueId+"|"+p.name);         }     } } 

All this does is loop through the clientList, obtain each of the player's details, and checks that the player p that was obtained is not this client's player player. If it isn't, send a MSG_ADD_NEW_PLAYER message with all the player details.

So the MSG_SET_NAME message is dealt with. Let's now have a look at the other message that the server needs to handle, which is the MSG_ MOVE_POSITION message that is sent by a client whenever the mouse is clicked within the bounds of the application window, It signifies that the player wants to move the circle to the location that was clicked on.

So again, we need to create a case for this message as follows:

case MSG_MOVE_POSITION: {

Once we get the message that a player has moved, we can simply retrieve the x, y position from the message using the next two lines of code:

player.x = Integer.parseInt(st.nextToken()); player.y = Integer.parseInt(st.nextToken());

Then we just need to broadcast this new position along with the unique ID of the player, so the clients can determine which player has moved. This can be seen in the following line of code. Notice that we also send the updated position to the client that is doing the moving. Most functionality should always be controlled through the server, as inevitably the server is the only one that can be trusted.

broadcast(MSG_MOVE_POSITION+"|"+player.uniqueId+"|"+player.x+     "|"+player.y);

That's all the message handling, so now let's take a final look at the disconnect method, which we have mentioned a couple of times previously. This method is used to clean up a client that has been disconnected for some reason or another. A client disconnecting can occur from possible IOException exceptions being thrown from either the IncomingMessageHandler thread or the OutgoingMessageHandler thread. With this in mind, we must handle calling the disconnect method from both and handle terminating both threads from here on. Note also that the disconnect method is synchronized so that it cannot be invoked by the outgoing message and incoming message threads at the same time.

We first check that the client is connected by checking our Boolean flag connected, using the following line of code:

if(connected)

This is so that if one of the outgoing or incoming message threads has handled disconnecting, the other doesn't need to if it tries.

If it is currently connected, remove the client from the clientList and then broadcast a message to all the other players informing them that this player has been disconnected. In the client, the player will be removed from the player lists, as we shall see later. (Note that we send the player's unique ID here also, so the client applications can determine which player has left the game.) The code to do this can be seen here:

synchronized(clientList) {     clientList.remove(this); }   broadcast(MSG_REMOVE_PLAYER+"|"+player.uniqueId);

Again, also note the synchronization with the clientList, as we are removing a client from it, which should be mutually exclusive to any other operations on the clientList ArrayList.

Next, we set the connected flag to false, which can be seen here:

connected = false;

Then we call the destroy method of both the incomingMessageHandler and the outgoingMessageHandler objects, so both their threads stop. This can be seen here.

incomingMessageHandler.destroy(); outgoingMessageHandler.destroy();

Then finally we attempt to close the socket and set its reference to null. That's our complete server. If you now run the code, you should see the following in a console window, which is stunning (we're sure you'll agree):

click to expand
Figure 17-6: The game server console application

Creating the Client

Now that we have our server ready, let's look at how we can make the client, which we can connect multiple instances of to the server. For this example, we are using the ActiveRendering example code from Chapter 9 as a base. You will also need the EventProcessor and EventProcessable source files from the end of Chapter 10 and the Protocol.java and Player.java source files we created for the server as we will be reusing them for the client. The remaining four source code files for this example are SampleClient (main class), NetworkHandler, NetworkListener, and NetworkEvent. The client application consists of eight source files in total.

Before we actually look at the main parts of the code, there are two foundation classes that we need to see first, which will be used in the core of our client network framework. These classes are the NetworkListener and the NetworkEvent. If you remember from our brief discussion before, we are going to deal with network events in the same manner as mouse and keyboard events, so we can easily synchronize them with the main loop and process them in order with other events received. To do this, we are going to need to actually create our own NetworkEvent class that extends the AWTEvent class, so let's have a look at our definition for the NetworkEvent class.

Code Listing 17-9: NetworkEvent.java

start example
import java.awt.*;   public class NetworkEvent extends AWTEvent {     public NetworkEvent(Object source, int id, String message)     {         super(source, id);         this.message = message;     }       public String message;          public static final int NETWORK_MESSAGE_RECEIVED = 2000;     public static final int NETWORK_DISCONNECTED = 2001; }
end example

There's nothing really complicated here; we simply extend the AWTEvent class and take in the standard source Object and id, which the AWTEvent constructor requires (when we call the super class constructor), and also the network message (which is simply a String object). In the constructor, we then call the super constructor (i.e., the constructor of the AWTEvent class) and we store the reference to the message within an instance member called message. Note also that we have two types of events defined in the class called NETWORK_MESSAGE_ RECEIVED and NETWORK_DISCONNECTED, in the same way that we can have MOUSE_PRESSED and MOUSE_RELEASED events, etc. We can use these static members later for comparison to the event's ID, similarly to how we do with other events when handling the events in the main loop. Note that the reason that we have assigned the event values as 2000 and 2001 is so that they do not conflict with any of the other AWTEvent IDs, such as the MouseEvent IDs like MOUSE_PRESSED, etc.

So that's all there is to our NetworkEvent. Let's also have a quick look at the NetworkListener interface that we require. Here is the complete listing for it:

Code Listing 17-10: NetworkListener.java

start example
public interface NetworkListener {     public void networkMessageReceived(NetworkEvent e);     public void networkDisconnected(NetworkEvent e); }
end example

This is easy really; all we have done here is create two methods that can be overridden to handle the two different types of events in the same way that a MouseListener has methods such as mouseClicked and mousePressed.

Let's now move on by looking at the NetworkHandler class. Note that this is quite similar to the ClientHandler class that we used for the server. Let's look at the complete code listing now.

Code Listing 17-11: NetworkHandler.java

start example
import java.net.*; import java.io.*; import java.util.*; import java.awt.*;     public class NetworkHandler {     public NetworkHandler(String address, int port, NetworkListener         listener)     {         try         {             socket = new Socket(address, port);                          this.listener = listener;                          DataInputStream in = new DataInputStream                 (socket.getInputStream());             DataOutputStream out = new DataOutputStream                 (socket.getOutputStream());                          incomingMessageHandler = new IncomingMessageHandler(in);             outgoingMessageHandler = new OutgoingMessageHandler(out);                          connected = true;           }         catch(IOException e)         {                         System.out.println("Unable to connect: "+e);             listener.networkDisconnected(new NetworkEvent                 (this, NetworkEvent.NETWORK_DISCONNECTED, null));         }     }               public class IncomingMessageHandler implements Runnable     {         public IncomingMessageHandler(DataInputStream in)         {             inStream = in;             receiver = new Thread(this);             receiver.start();         }              public void run()         {             Thread thisThread = Thread.currentThread();             while(receiver==thisThread)             {                 try                 {                     String message = inStream.readUTF();                     listener.networkMessageReceived(new NetworkEvent                         (this, NetworkEvent.NETWORK_MESSAGE_RECEIVED,                          message));                 }                 catch(IOException e)                 {                     disconnect();                 }             }         }                  public void destroy()         {             receiver = null;         }                  Thread receiver;         private DataInputStream inStream;     }               public class OutgoingMessageHandler implements Runnable     {         public OutgoingMessageHandler(DataOutputStream out)         {             outStream = out;             messageList = new LinkedList();                          sender = new Thread(this);             sender.start();         }                  public void addMessage(String message)         {             synchronized(messageList)             {                 messageList.add(message);                 messageList.notify();             }         }                  public void run()         {             String message;                          Thread thisThread = Thread.currentThread();             while(sender==thisThread)             {                 synchronized(messageList)                 {                     if(messageList.isEmpty() && sender!=null)                     {                         try                         {                             messageList.wait();                         }                         catch(InterruptedException e) { }                     }                 }                                  while(messageList.size()>0)                 {                     synchronized(messageList)                     {                         message = (String)messageList.removeFirst();                     }                                          try                     {                         outStream.writeUTF(message);                     }                     catch(IOException e)                     {                         disconnect();                     }                 }             }                 }                  public void destroy()         {             sender = null;                          synchronized(messageList)             {                 messageList.notify();                 // wake up if stuck in waiting stage             }         }                  Thread sender;         LinkedList messageList;         DataOutputStream outStream;     }               public synchronized void disconnect()     {         if(connected)         {             connected = false;             incomingMessageHandler.destroy();             outgoingMessageHandler.destroy();                      try             {                 socket.close();             }             catch(Exception e) {}             socket = null;                          listener.networkDisconnected(new NetworkEvent(this,                 NetworkEvent.NETWORK_DISCONNECTED, null));         }     }                 public void sendMessage(String message)     {         outgoingMessageHandler.addMessage(message);     }             private Socket socket;         private IncomingMessageHandler incomingMessageHandler;     private OutgoingMessageHandler outgoingMessageHandler;         private NetworkListener listener;     public boolean connected; } 
end example

Let's look at the differences between this and the ClientHandler class that we made for the server. First, for the parameters in the constructor, we will take in a string that represents either the IP address or machine name to which we wish to connect, followed by the port that the server will be running on and a reference to an object that implements the NetworkListener interface.

In the constructor, we first attempt to create a Socket object by passing in the address and port into the socket class constructor. This will establish a connection to the server and can be seen in the following line of code:

socket = new Socket(address, port);

Next we store a reference to the object that implements the NetworkListener interface in an instance member called listener, which can be seen here:

this.listener = listener;

The rest of the constructor is then the same as the server (i.e., creating the incoming and outgoing message handlers). However, note that if we get an IOException in the constructor (i.e., the client could not connect to the server), we create a new NetworkEvent object and then pass it as a parameter to the networkDisconnected method of the listener. This can be seen here:

listener.networkDisconnected(new NetworkEvent(this,      NetworkEvent.NETWORK_DISCONNECTED, null));

Another difference between this and the ClientHandler of the server is that we don't have broadcast and broadcastFromClient methods, as messages can only be sent to the server. In addition, instead of having a handleMessage method and calling it in the IncomingMessageHandler class, we instead create a new NetworkEvent object each time we receive a message and then call the networkMessageReceived method of the Listener object, passing in the new NetworkEvent object with the message in it. This can be seen here:

listener.networkMessageReceived(new NetworkEvent(this,      NetworkEvent.NETWORK_MESSAGE_RECEIVED, message));

The final change is that at the end of the disconnect method, we call the listener's networkDisconnected method, passing a network disconnected event to it. This can be seen here:

listener.networkDisconnected(new NetworkEvent(this,      NetworkEvent.NETWORK_DISCONNECTED, null)); 

Now let's look at how we implement all of this in the actual application. Here is the complete code listing for the SampleClient class, which contains our main method.

Code Listing 17-12: SampleClient.java

start example
import java.awt.*; import java.awt.image.*; import javax.swing.*; import java.awt.event.*; import java.util.*;   public class SampleClient extends JFrame implements Runnable,                             EventProcessable,                             NetworkListener,                             MouseListener,                             Protocol {     public SampleClient()     {         setTitle("Sample Network Client - "+playerName);         getContentPane().setLayout(null);         setResizable(false);         setIgnoreRepaint(true);                  addWindowListener(new WindowAdapter() {                           public void windowClosing(WindowEvent e) {                           exitProgram();                           }                         });                                          backBuffer = new BufferedImage(DISPLAY_WIDTH, DISPLAY_HEIGHT,             BufferedImage.TYPE_INT_RGB);         bbGraphics = (Graphics2D) backBuffer.getGraphics();           playerList = new ArrayList();             eventProcessor = new EventProcessor(this);         getContentPane().addMouseListener(this);             networkHandler = new NetworkHandler("127.0.0.1", 9000, this);             setVisible(true);             Insets insets = getInsets();         DISPLAY_X = insets.left;         DISPLAY_Y = insets.top;         resizeToInternalSize(DISPLAY_WIDTH, DISPLAY_HEIGHT);     }               public void resizeToInternalSize(int internalWidth, int         internalHeight)     {         Insets insets = getInsets();         final int newWidth = internalWidth + insets.left              + insets.right;         final int newHeight = internalHeight + insets.top              + insets.bottom;                  Runnable resize = new Runnable()         {             public void run()             {                 setSize(newWidth, newHeight);             }         };                  if(!SwingUtilities.isEventDispatchThread())         {             try             {                 SwingUtilities.invokeAndWait(resize);             }             catch(Exception e) {}         }         else             resize.run();                  validate();     }          public void run()     {         long startTime, waitTime, elapsedTime;         //    1000/25 Frames Per Second = 40 millisecond delay         int delayTime = 1000/25;                  Thread thisThread = Thread.currentThread();         while(loop==thisThread)         {             startTime = System.currentTimeMillis();                          // process received events             eventProcessor.processEventList();                 // render to back buffer now             render(bbGraphics);                          // render back buffer image to screen             Graphics g = getGraphics();             g.drawImage(backBuffer, DISPLAY_X, DISPLAY_Y, null);             g.dispose();                                       //  handle frame rate             elapsedTime = System.currentTimeMillis() - startTime;             waitTime = Math.max(delayTime - elapsedTime, 5);                          try             {                  Thread.sleep(waitTime);              }             catch(InterruptedException e) {}         }                  System.out.println("Program Exited");                  dispose();         System.exit(0);     }         public void render(Graphics g)     {         g.clearRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);                  // render all players...                  Player p;                  for(int i=0; i<playerList.size(); i++)         {             p = (Player)playerList.get(i);             g.setColor(Player.colors[p.colId]);             g.fillOval(p.x-10, p.y-10, 20, 20);             g.drawString(p.name, p.x+20, p.y);         }     }               public void handleEvent(AWTEvent e)     {         switch(e.getID())         {             case MouseEvent.MOUSE_PRESSED:             {                 System.out.println("Mouse Pressed Event");                 MouseEvent mouseEvent = (MouseEvent)e;                 networkHandler.sendMessage(MSG_MOVE_POSITION+"|"+                     mouseEvent.getX()+"|"+mouseEvent.getY());                 break;             }                 case NetworkEvent.NETWORK_MESSAGE_RECEIVED:                                  System.out.println("Network Message Received:"                      + ((NetworkEvent)e).message);                 handleNetworkMessage(((NetworkEvent)e).message);                 break;                 case NetworkEvent.NETWORK_DISCONNECTED:             {                 System.out.println("Network Disconnected");                 exitProgram();                 break;             }         }     }          public void handleNetworkMessage(String message)     {         StringTokenizer st = new StringTokenizer(message, "|");         int type = Integer.parseInt(st.nextToken());                  switch(type)         {             case MSG_INIT_PLAYER:                 player = new Player(Integer.parseInt(st.nextToken()),                                Integer.parseInt(st.nextToken()),                                Integer.parseInt(st.nextToken()),                                Integer.parseInt(st.nextToken()));                              networkHandler.sendMessage(MSG_SET_NAME+"|"+playerName);                 break;                              case MSG_SET_NAME:                    player.name = st.nextToken();                    playerList.add(player);                    break;                            case MSG_ADD_NEW_PLAYER:            {                Player p = new Player(Integer.parseInt                    (st.nextToken()),                           Integer.parseInt(st.nextToken()),                           Integer.parseInt(st.nextToken()),                           Integer.parseInt(st.nextToken()));                          p.name = st.nextToken();             playerList.add(p);             break;             }                          case MSG_MOVE_POSITION:             {                 Player p;                 int id = Integer.parseInt(st.nextToken());                 for(int i=0; i<playerList.size(); i++)                 {                     p = (Player)playerList.get(i);                     if(p.uniqueId==id)                     {                         p.x = Integer.parseInt(st.nextToken());                         p.y = Integer.parseInt(st.nextToken());                         break;                     }                 }                              break;             }                          case MSG_REMOVE_PLAYER:             {                 Player p;                 int id = Integer.parseInt(st.nextToken());                 for(int i=0; i<playerList.size(); i++)                 {                     p = (Player)playerList.get(i);                     if(p.uniqueId==id)                     {                         playerList.remove(i);                         break;                     }                 }             }         }     }          public void networkMessageReceived(NetworkEvent e)     {         eventProcessor.addEvent(e);     }               public void networkDisconnected(NetworkEvent e)     {         eventProcessor.addEvent(e);     }              public void mousePressed(MouseEvent e)     {         eventProcessor.addEvent(e);     }              public void mouseReleased(MouseEvent e) {}     public void mouseClicked(MouseEvent e) {}     public void mouseEntered(MouseEvent e) {}     public void mouseExited(MouseEvent e) {}                      public void exitProgram()     {         loop = null;     }          public static void main(String args[])     {         // get the players name...         playerName = JOptionPane.showInputDialog(null,              "Please enter your name:");                  // start 'I'm a Circle'!         SampleClient app = new SampleClient();             app.loop = new Thread(app);         app.loop.start();     }          private EventProcessor eventProcessor;     private NetworkHandler networkHandler;          private Thread loop;     private BufferedImage backBuffer;     private Graphics2D bbGraphics;          private final int DISPLAY_X; // value assigned in constructor     private final int DISPLAY_Y; // value assigned in constructor     private static final int DISPLAY_WIDTH = 640;     private static final int DISPLAY_HEIGHT = 480;          public Player player;     public ArrayList playerList;     public static String playerName; }
end example

So let's look at what we have added to the ActiveRendering example to make it into the absolute best seller "I'm a circle game" (although "game" might not be the right word here).

First, in the main method, we pop up an input dialog to get the player's name, using the static method showInputDialog in the JOptionPane class. This can be seen here:

// get the players name... playerName = JOptionPane.showInputDialog(null,      "Please enter your name:");

As you can see, we store the result of this in a static string variable called playerName. After we have the player's name, we create our application in the usual manner. Let's look now at what we have added into the constructor.

First, we have initialized an ArrayList called playerList, which will hold a list of Player objects (i.e., all the players that are connected, as well as the client's own player).

Next, we create an eventProcessor object using the EventProcessor class that we created back in Chapter 10, and we add a MouseListener to the content pane of our JFrame. This can be seen in the following two lines of code:

eventProcessor = new EventProcessor(this); getContentPane().addMouseListener(this);

Then we create a new NetworkHandler object called networkHandler, passing in the IP address and port of the server that we wish to connect to, as well as a reference to our SampleClient object that we created, as it implements the NetworkListener interface. This can be seen here:

networkHandler = new NetworkHandler("127.0.0.1", 9000, this);

The NetworkListener that we have created works in a similar way to the key and mouse listener methods, notifying our network listener methods when network events occur, as we shall see.

At this point, we should be connected to the server. The client will then proceed into the main game loop, calling the eventProcessor.processEventList() and render() methods every loop.

What happens now then? Well, if you remember back to when we created the server, when a client connects, the server sends a MSG_ INIT_PLAYER message to the client. So the client should look out for the arrival of this message. If you remember back to the NetworkHandler class, when this message arrives, it will call the networkMessageReceived method of the Listener object that was passed into the NetworkHandler class, which in this example is our main class SampleClient; so we need to define the networkMessageReceived method in our main class SampleClient, as well as the other method of the NetworkListener interface, networkDisconnected, which receives events if the connection to the server is lost. These two methods can be seen here:

public void networkMessageReceived(NetworkEvent e) {     eventProcessor.addEvent(e); }      public void networkDisconnected(NetworkEvent e) {     eventProcessor.addEvent(e); }

All we are doing in these methods is adding the events to our eventProcessor just as we added other events, such as mouse and keyboard events. Also, notice how we have defined the mousePressed method with the following method, as we need to move the player (Circle) through this input:

public void mousePressed(MouseEvent e) {     eventProcessor.addEvent(e); }

So in this example, our event processor will be receiving both mouse events and network events, although in a more complex example it could also be handling keyboard and focus events. However, we have left these out to keep things as simple as possible.

These events will then be processed when the processEventList method of our eventProcessor object is called in the main loop thread (nicely synchronizing all our events with the main loop). We now need to define the handleEvent method of the EventProcessable interface. The entire method that we have created can be seen here:

public void handleEvent(AWTEvent e) {     switch(e.getID())     {         case MouseEvent.MOUSE_PRESSED:         {             System.out.println("Mouse Pressed Event");             MouseEvent mouseEvent = (MouseEvent)e;             networkHandler.sendMessage(MSG_MOVE_POSITION+"|"                 +mouseEvent.getX()+"|"+mouseEvent.getY());             break;         }           case NetworkEvent.NETWORK_MESSAGE_RECEIVED:                          System.out.println("Network Message Received:"                  + ((NetworkEvent)e).message);             handleNetworkMessage(((NetworkEvent)e).message);             break;           case NetworkEvent.NETWORK_DISCONNECTED:         {             System.out.println("Network Disconnected");             exitProgram();             break;         }     } } 

In this method, we simply switch the ID of the message and create cases for each message in which we are interested. For the MOUSE_PRESSED message, we send a message to the server using the sendMessage method of the networkHandler object where the player clicked, so the server can then update the player's position and inform all players as to the new position of the player.

For NETWORK_MESSAGE_RECEIVED events, we have created another method within our SampleClient class to deal with the actual message, as it would be quite messy to put the entire message handling code in here. So all we do is cast the AWTEvent down to being a NetworkEvent and pass the actual string message into the handleNetworkMessage method, which we will look at in a moment.

Finally, we have the NETWORK_DISCONNECTED message, which means we want to simply quit the application if we receive it. Note here, however, that we could add code to attempt a reconnection to the server by simply creating a new NetworkHandler object again. If the connection failed again, it would send another NETWORK_DISCONNECTED message.

Let's now have a look at the handleNetworkMessage method. In this method, we first tokenize the message and get the type of message as an integer value, as we did in the server code. This can be seen here (note we implement the same Protocol interface in the client as we did in the server so that we have the same message definitions):

StringTokenizer st = new StringTokenizer(message, "|"); int type = Integer.parseInt(st.nextToken());

Next we create a switch statement for the type of message that we have received. As we mentioned before, once the client has connected to the server, the server will send the client a MSG_INIT_PLAYER message, which contains information such as the initial position of the player, the color ID, and also the unique ID for the player. Therefore, we can create a case for this message as follows:

switch(type) {         case MSG_INIT_PLAYER:

Then, within the case, we want to create a new Player object called player using the values sent from the server. Since we have sent them in the same order from the server as it requires them into the constructor of the player class, we can pass the tokens in directly to the constructor. This can be seen here:

player = new Player(Integer.parseInt(st.nextToken()),                           Integer.parseInt(st.nextToken()),                           Integer.parseInt(st.nextToken()),                           Integer.parseInt(st.nextToken())); 

Once the player object is created, we send the server a MSG_SET_NAME message to set the name of the player. If you remember from when we created the server, the server will send out messages to all the players about the existence of this player and send this player information about all of the other players currently connected to the server. The code to send the name of the player can be seen here:

networkHandler.sendMessage(MSG_SET_NAME+"|"+playerName);

Once this is sent, the server will then send a message back confirming that the name has been set. This will then update it in the player object and add the player object to the player list, and the rendering code will start drawing your player to the screen (we will look at the render method soon). The handling code for the MSG_SET_NAME message can be seen here:

case MSG_SET_NAME:     player.name = st.nextToken();     playerList.add(player);     break;

Note that all of this code does not need to be synchronized explicitly because everything is running in the main loop; so everything is synchronized already.

The next message that we have to handle is when a new player is added to the game. Note that this message is also used when you have sent your name to the server when the server in turn sends you back all of the players that are currently in the game on the server. Let's look at the complete code for the case to handle the MSG_ADD_NEW_PLAYER message now.

case MSG_ADD_NEW_PLAYER: {     Player p = new Player(Integer.parseInt(st.nextToken()),                           Integer.parseInt(st.nextToken()),                           Integer.parseInt(st.nextToken()),                           Integer.parseInt(st.nextToken()));                          p.name = st.nextToken();         playerList.add(p);         break; }

As you can see, all we do here is create a new Player object from the data contained within the tokens of the string (which also has the player's unique ID), assign the name, and then add the player to the playerList ArrayList.

The next message that we handle here is the MSG_MOVE_POSITION, which is sent whenever any player moves. We can tell which player has actually moved (whether it be ourselves or another player) by looking at the unique ID that was also sent along with the new x and y position of the player that moved.

We first get the id (of the player that has moved) from the message using the following line of code:

int id = Integer.parseInt(st.nextToken()); 

Then we loop through the playerList until we find the player with the matching unique ID. When we find the correct player, we simply set the new x and y values and then break out of the for loop. This can be seen here:

for(int i=0; i<playerList.size(); i++) {     p = (Player)playerList.get(i);     if(p.uniqueId==id)     {         p.x = Integer.parseInt(st.nextToken());         p.y = Integer.parseInt(st.nextToken());         break;     } }

The final message that we have to handle is when a player is disconnected from the server. The server sends this message automatically when it loses a connection from another client, and it also sends the unique ID of the player that has disconnected so we can use this to simply remove it from our playerList. The complete case for the MSG_REMOVE_ PLAYER message can be seen here:

case MSG_REMOVE_PLAYER: {     Player p;     int id = Integer.parseInt(st.nextToken());     for(int i=0; i<playerList.size(); i++)     {         p = (Player)playerList.get(i);         if(p.uniqueId==id)         {             playerList.remove(i);             break;         }     } }

All we need to look at now is the render method, which is where we draw all the players to the screen. Here is the complete code for the render method:

public void render(Graphics g) {     g.clearRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);              // render all players...              Player p;              for(int i=0; i<playerList.size(); i++)     {         p = (Player)playerList.get(i);         g.setColor(Player.colors[p.colId]);         g.fillOval(p.x-10, p.y-10, 20, 20);         g.drawString(p.name, p.x+20, p.y);     } } 

So all we do in the render method is loop through playerList and set the color using the color ID (colId) in the Player object p that was assigned by the server (by accessing the static colors array defined as follows in the player class).

public static final Color[] colors = {Color.red, Color.green,     Color.blue, Color.yellow, Color.magenta};

Then we draw a circle to represent the player using the fillOval method of the Graphics object g. Finally, we draw the player name to the right of the player using the drawString method.

Note again that we do not need to do any explicit synchronization here, as the rendering is performed actively in the main loop, along with all of the event handling code.

Now here are some images of "I'm a circle" in action!

click to expand
Figure 17-7: Everyone having fun playing "I'm a circle!" or not

click to expand
Figure 17-8: Joel then felt lonely as everyone moved away from him.

In general, a multiplayer game will usually have some kind of AI (artificial intelligence), be it enemy computer players or anything that the server needs to control, such as a game timer. The server application that we have made in this chapter did not have a main loop, thus it does not do any real-time processing of its own and is therefore an event-driven server, reacting merely to client messages and processing them when they arrive. For the server to have a main loop itself, simply follow the same steps that we used for the main loop in the client. Having the main loop system in the server will also mean that all event processing would be synchronized, so you would need to perform so much explicit synchronization with the client main loop system. You may also want to add a visual in-game display to the server for debugging purposes.



Java 1.4 Game Programming
Java 1.4 Game Programming (Wordware Game and Graphics Library)
ISBN: 1556229631
EAN: 2147483647
Year: 2003
Pages: 237

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net