|
The source code of all examples is included in the TcpJLib distribution.
Example 4: Synchronizing node events
In a multi-user game application it may be important that at any time all nodes reflects the same state as close as possible. This is a major challenge due to the delays when transmitting data over the internet. What you need is a clock in every node that runs synchronized as precise as possible with all other node clocks in absolute time (with an arbitrary zero). TcpJLib provides such a synchronization facility that is accurate to about 50 ms in most circumstances. This is achieved as follows:
The relay server runs a clock with microsecond precision and a node synchronizes its clock by calling a synchronization request. A separate TCP/IP socket is opened (on port 80) and the relay reports its clock time as a high-priority HTTP reply. The local clock is then set to the reported time corrected by half the measured time interval between sending the request and getting the reply. This assumes that the data transfer times from the node to the relay and back are equal and that the time to handle the request by the relay may be neglected. Our tests show synchronization errors in the order of 10 - 20 ms with nodes that are at different sites using different internet connections. (We used telephone links between nodes to establish an "absolute" timing. Better would be a RF link or a synchronization using GPS clocks.)
In the following example we want to compare the reaction time of different players. A game server "drops" apples that must be hit by the players to be removed. The first user that hits the apple gets the score and the apple disappears. To make the game fair, it is important that an apple is presented to every player at the same time as accurate as possible. The game server sends the DROP command consecutively to all the connected clients (via the relay). Without synchronization, the delay of the arrival at the clients nodes depends on the relay load, the number of participants and the internet transmission speed. It may be as large as some 100 ms, too much for a fair game. To circumvent this problem, the game server sends the DROP command together with a time stamp that informs the clients to delay the dropping action until the specified absolute time is reached.
The client reports the hit to the game server that sends a command to all players to remove the apple and to inform who got the hit. Only the first hit report is accepted. This may be incorrect because of different transmission times from the client to the server, but the error is in the order of some tens of milliseconds.
As you see in the following code, managing the connections and disconnections by the game server is very simple due to the notification callback notifyAgentConnection(). We use an ArrayList players of the nickname of all connected players that is used to send the data by sendCommand(). Data from agents is received by the callback pipeRequest(). The protocol is defined in the interface Command using integers:
interface Command
{
int INIT = 0;
int DROP = 1;
int HIT = 2;
int REMOVE = 3;
int TIME = 4;
}
When a player hits the apple, the HIT command is sent to the server that just registers the name of the client and disables further treatment of HIT commands by setting isRequestEnabled to false. To measure the reaction time, the client could determine it and transfer it together with the HIT command to the server.
Look at the dropToAll() method to see how the synchronization of dropping all apples together is performed: We request the current clock time and add a delay that is big enough to make sure that the DROP command arrived at all clients. The clients delay dropping the apple until the drop time sent by the server is reached. The DROP commands are generated by the server application thread. After a DROP command is sent, the thread is paused using Monitor.putSleep(). When a hit is reported by the pipeRequest() callback, the thread is requested to run and send the next DROP command by calling Monitor.wakeUp().
For debugging and demonstration purposes a server log is displayed in a Console window (from the package ch.aplu.util).
// HitMeServer.java
import ch.aplu.util.*;
import ch.aplu.tcp.*;
import java.util.*;
public class HitMeServer extends Console
{
private final String sessionID = "hitme7912302042";
private final String bridgeName = "HitMeServer";
private TcpBridge bridge = new TcpBridge(sessionID, bridgeName);
private ArrayList<String> players = new ArrayList<String>();
private boolean isRequestEnabled = true;
private long dropTimeLong;
public HitMeServer()
{
bridge.addTcpBridgeListener(new TcpBridgeAdapter()
{
public void pipeRequest(String source, String destination, int[] data)
{
if (!isRequestEnabled)
return;
isRequestEnabled = false; // Disable subsequent hits
println("Got hit from " + source);
long elapsedTimeLong = bridge.getClockTime() - dropTimeLong;
removeToAll(source, elapsedTimeLong);
Monitor.wakeUp();
}
public void notifyAgentConnection(String agentName, boolean connected)
{
println(agentName + (connected ? " connected" : " disconnected"));
if (connected)
players.add(agentName);
else
players.remove(agentName);
if (!players.isEmpty())
{
initToAll();
Monitor.wakeUp();
}
}
});
print("Connecting to relay...");
bridge.connectToRelay();
println("done.\nWaiting for players...");
bridge.sendSynchRequest();
while (true)
{
Monitor.putSleep();
TcpTools.delay((int)(1000 + 3000 * Math.random()));
int x = (int)(10 * Math.random());
int y = (int)(10 * Math.random());
dropToAll(x, y);
}
}
private void initToAll()
{
println("Game initialized");
for (String agent : players)
bridge.sendCommand("", agent, Command.INIT);
}
private void dropToAll(int x, int y)
{
println("Drop at (x, y) = (" + x + ", " + y + ")");
dropTimeLong = bridge.getClockTime() + 500; // 500 ms delay!
int[] dropTime = TcpTools.toIntAry(dropTimeLong);
for (String agent : players)
bridge.send("", agent, Command.DROP, dropTime[0], dropTime[1], x, y);
isRequestEnabled = true;
}
private void removeToAll(String source, long elapsedTimeLong)
{
for (String agent : players)
{
bridge.sendCommand("", agent, Command.REMOVE,
TcpTools.stringToIntAry(source));
bridge.sendCommand("", agent, Command.TIME,
TcpTools.toIntAry(elapsedTimeLong));
}
}
public static void main(String[] args)
{
new HitMeServer();
}
} |
The HitMePlayer class is derived from GameGrid to give immediate access to all its methods. HitMePlayer "has-a" TcpAgent that does all transmission stuff. It receives data from the game server in the dataReceived() callback defined in the TcpAgentListener interface. We assume that the first integer in the transferred data is a command tag, so we can use a switch selection to treat the different commands. Look at the Command.DROP case to see that, as stated before, we wait to drop the apple until the transmitted drop time is reached.
When you connect to the relay, it replies by giving you a "connection list" that contains all agents already connected. The first entry is the personal name attributed to your node. The personal name is your requested nickname eventually appended by a (n) trailer, if the nickname is already in use. Because all agents use the same nickname "HitMePlayer", this happens when more than one player connects. To make the game more individually, you may ask for a user name at the program start.
// HitMePlayer.java
import ch.aplu.jgamegrid.*;
import ch.aplu.tcp.*;
import java.awt.*;
public class HitMePlayer extends GameGrid implements GGMouseListener
{
private final String sessionID = "hitme7912302042";
private final String bridgeName = "HitMeServer";
private String agentName = "HitMePlayer";
private String personalName;
private TcpAgent agent = new TcpAgent(sessionID, bridgeName);
private String hitter;
private int nbHits = 0;
public HitMePlayer()
{
super(10, 10, 50, Color.green, false);
addMouseListener(this, GGMouse.lPress);
addStatusBar(30);
agent.addTcpAgentListener(new TcpAgentAdapter()
{
public void dataReceived(String source, int[] data)
{
switch (data[0])
{
case Command.INIT:
removeAllActors();
nbHits = 0;
setStatusText("Game initialized");
refresh();
break;
case Command.DROP:
long clockTime = TcpTools.toLong(data[1], data[2]);
while (agent.getClockTime() < clockTime)
{
}
Actor apple = new Actor("sprites/apple.gif");
addActor(apple, new Location(data[3], data[4]));
break;
case Command.REMOVE:
removeAllActors();
refresh();
hitter = TcpTools.intAryToString(data, 1);
if (hitter.equals(personalName))
nbHits++;
break;
case Command.TIME:
Long reactionTime = TcpTools.toLong(data[1], data[2]);
setStatusText("Hitter: " + hitter + ". Reaction time: "
+ reactionTime + " ms. Your score: " + nbHits);
break;
}
}
});
show();
personalName = agent.connectToRelay(agentName).get(0);
setTitle("Personal name: " + personalName);
setStatusText("Connection established. Personal name attributed: "
+ personalName);
agent.sendSynchRequest();
}
public boolean mouseEvent(GGMouse mouse)
{
Location location = toLocationInGrid(mouse.getX(), mouse.getY());
Actor a = getOneActorAt(location);
if (a != null)
{
a.removeSelf();
agent.sendCommand("", Command.HIT);
refresh();
}
return true;
}
public static void main(String[] args)
{
new HitMePlayer();
}
}
|
Execute the game server program first locally using WebStart.
Execute the player program locally using WebStart.
You may start as many player instances as you want from the same or different computers.
(These applications are slightly replenished with more complete user score information and the measurement of the reaction time. See the source code in the TcpJLib distribution.)
Typical application based on the technique explained above: AppleShooter
Wilhelm Tell is a folk hero of Switzerland. His legend is recorded in a late 15th century. On November 1307, Tell split an apple on his son's head with a bolt from his crossbow. This is a training application if you want to follow in Tell's footsteps.
[Ref.: http://en.wikipedia.org/wiki/William_Tell]
Execute the game server program first using WebStart. Enter a session ID that is unique for your game.
Execute the player program for two players using WebStart. Use the same session ID as the server.
Use cursor up/down to target, space key to shoot.
The two players and the server may be located anywhere on the world.
| |