Wednesday, January 20, 2010

Watching the Server









Watching the Server


A TourWatcher tHRead monitors the server's output, waiting for messages. The message types are listed below with a brief description of what the TourWatcher does in response:



create n x z


Create a distributed sprite in the local world with name n at position (x, 0, z). By default, the sprite will face forward along the positive z-axis.



wantDetails A P


The client at IP address A and port P is requesting information about the local sprite on this machine. Gather the data and send the data back in a "detailsFor" message.



detailsFor n1 x1 z1 r


Create a distributed sprite with the name n1 at location (x1,0,z1), rotated r radians away from the positive z-axis.



n <move or rotation command>


<command> can be one of forward, back, left, right, rotCClock, or rotClock. The distributed sprite with name n is moved or rotated. rotCClock is a counter-clockwise rotation, and rotClock is clockwise.



n bye


A client has left the world, so the distributed sprite with name n is detached (deleted).



The activities using these messages are shown in Figures Figure 32-6, 32-7, and 32-8.


Almost all the messages are related to distributed sprites in the local world: creation, movement, rotation, and deletion. Therefore, these tasks are handled by TourWatcher, which maintains its sprites in a HashMap, mapping sprite names to DistTourSprite objects:



private HashMap visitors; // stores (name, sprite object) pairs



The run( ) method in TourWatcher accepts a message from the server and tests the first word in the message to decide what to do:



public void run( )
{ String line;
try {
while ((line = in.readLine( )) != null) {
if (line.startsWith("create"))
createVisitor( line.trim( ) );
else if (line.startsWith("wantDetails"))
sendDetails( line.trim( ) );
else if (line.startsWith("detailsFor"))
receiveDetails( line.trim( ) );
else
doCommand( line.trim( ) );
}
}
catch(Exception e) // socket closure causes termination of while
{ System.out.println("Link to Server Lost");
System.exit( 0 );
}
}




Creating a Distributed Sprite


A distributed sprite is made in response to a create n x z message by creating a DistTourSprite object with name n at location (x, 0, z) oriented along the positive z-axis. The name and sprite object are stored in the visitors HashMap for future reference.



private void createVisitor(String line)
{
StringTokenizer st = new StringTokenizer(line);


st.nextToken( ); // skip "create" word
String userName = st.nextToken( );
double xPosn = Double.parseDouble( st.nextToken( ) );
double zPosn = Double.parseDouble( st.nextToken( ) );

if (visitors.containsKey(userName))
System.out.println("Duplicate name -- ignoring it");
else {
DistTourSprite dtSprite =
w3d.addVisitor(userName, xPosn, zPosn, 0);
visitors.put( userName, dtSprite);
}
}



A potential problem is if the proposed name has been used for another sprite. TourWatcher only prints an error message to standard output; it would be better if a message was sent back to the originating client.




The Distributed Sprites Class


DistTourSprite is a simplified version of TourSprite: its sprite movement and rotation interface is the same as TourSprite's, but DistTourSprite doesn't send messages to the server. The complete class appears in Example 32-2.



Example 32-2. The DistTourSprite class


public class DistTourSprite extends Sprite3D
{
private final static double MOVERATE = 0.3;
private final static double ROTATE_AMT = Math.PI / 16.0;

public DistTourSprite(String userName, String fnm, Obstacles obs,
double xPosn, double zPosn)
{ super(userName, fnm, obs);
setPosition(xPosn, zPosn);
}

// moves
public boolean moveForward( )
{ return moveBy(0.0, MOVERATE); }

public boolean moveBackward( )
{ return moveBy(0.0, -MOVERATE); }

public boolean moveLeft( )
{ return moveBy(-MOVERATE,0.0); }

public boolean moveRight( )
{ return moveBy(MOVERATE,0.0); }

// rotations in Y-axis only
public void rotClock( )
{ doRotateY(-ROTATE_AMT); } // clockwise


public void rotCounterClock( )
{ doRotateY(ROTATE_AMT); } // counter-clockwise

} // end of DistTourSprite class






Moving and Rotating a Distributed Sprite


doCommand( ) in TourWatcher distinguishes between the various move and rotation messages and detects bye:



private void doCommand(String line)
{
StringTokenizer st = new StringTokenizer(line);
String userName = st.nextToken( );
String command = st.nextToken( );

DistTourSprite dtSprite =
(DistTourSprite) visitors.get(userName);
if (dtSprite == null)
System.out.println(userName + " is not here");
else {
if (command.equals("forward"))
dtSprite.moveForward( );
else if (command.equals("back"))
dtSprite.moveBackward( );
else if (command.equals("left"))
dtSprite.moveLeft( );
else if (command.equals("right"))
dtSprite.moveRight( );
else if (command.equals("rotCClock"))
dtSprite.rotCounterClock( );
else if (command.equals("rotClock"))
dtSprite.rotClock( );
else if (command.equals("bye")) {
System.out.println("Removing info on " + userName);
dtSprite.detach( );
visitors.remove(userName);
}
else
System.out.println("Do not recognise the command");
}
} // end of doCommand( )



All of the commands start with the sprite's name, which is used to look up the DistTourSprite object in the visitors HashMap. If the object cannot be found then TourWatcher notifies only the local machine; it should probably send an error message back to the original client.


The various moves and rotations are mapped to calls to methods in the DistTourSprite object. The bye message causes the sprite to be detached from the local world's scene graph and removed from the HashMap.




Responding to Sprite Detail Requests


Figure 32-6 shows that a wantDetails A P message causes TourWatcher to collect information about the sprite local to this machine. The details are sent back as a detailsFor A P x1 z1 r message to the client at IP address A and port P. The information states that the sprite is currently positioned at (x1, 0, z1) and rotated r radians away from the positive z-axis.


TourWatcher doesn't manage the local sprite, so passes the wantDetails request to the WrapNetTour3D object for processing:



private void sendDetails(String line)
{ StringTokenizer st = new StringTokenizer(line);
st.nextToken( ); // skip 'wantDetails' word
String cliAddr = st.nextToken( );
String strPort = st.nextToken( ); // don't parse

w3d.sendDetails(cliAddr, strPort);
}



sendDetails( ) in WrapNetTour3D accesses the local sprite (referred to as bob) and constructs the necessary reply:



public void sendDetails(String cliAddr, String strPort)
{ Point3d currLoc = bob.getCurrLoc( );
double currRotation = bob.getCurrRotation( );
String msg = new String("detailsFor " + cliAddr + " " +
strPort + " " +
df.format(currLoc.x) + " " +
df.format(currLoc.z) + " " +
df.format(currRotation) );
out.println(msg);
}



The (x, z) location is formatted to four decimal places to reduce the length of the string sent over the network.




Receiving Other Client's Sprite Details


Figure 32-6 shows that when a user joins the world, it will be sent detailsFor messages by the existing clients. Each of these messages is received by TourWatcher, and leads to the creation of a distributed sprite.


TourWatcher's receiveDetails( ) method pulls apart a detailsFor n1 x1 z1 r message and creates a DistTourSprite with name n1 at (x1, 0, z1) and rotation r:



private void receiveDetails(String line)
{
StringTokenizer st = new StringTokenizer(line);

st.nextToken( ); // skip 'detailsFor' word
String userName = st.nextToken( );
double xPosn = Double.parseDouble( st.nextToken( ) );

double zPosn = Double.parseDouble( st.nextToken( ) );
double rotRadians = Double.parseDouble( st.nextToken( ) );

if (visitors.containsKey(userName))
System.out.println("Duplicate name -- ignoring it");
else {
DistTourSprite dtSprite =
w3d.addVisitor(userName, xPosn, zPosn, rotRadians);
visitors.put( userName, dtSprite);
}
}



The new sprite must be added to the local world's scene graph, so it is created in WrapNetTour3D by addVisitor( ):



public DistTourSprite addVisitor(String userName,
double xPosn, double zPosn, double rotRadians)
{
DistTourSprite dtSprite =
new DistTourSprite(userName,"Coolrobo.3ds", obs, xPosn, zPosn);
if (rotRadians != 0)
dtSprite.setCurrRotation(rotRadians);

BranchGroup sBG = dtSprite.getBG( );
sBG.compile( ); // generally a good idea
try {
Thread.sleep(200); // delay a little, so world is finished
}
catch(InterruptedException e) {}
sceneBG.addChild( sBG );

if (!sBG.isLive( )) // just in case, but problem seems solved
System.out.println("Visitor Sprite is NOT live");
else
System.out.println("Visitor Sprite is now live");

return dtSprite;
}



Two important elements of this code are that the sub-branch for the distributed sprite is compiled, and the method delays for 200 ms before adding it to the scene. Without these extras, the new BranchGroup, sBG, sometimes fails to become live, which means that it subsequently cannot be manipulated (e.g., its TRansformGroup cannot be adjusted to move or rotate the sprite).


The problem appears to be due to the threaded nature of the client: WrapNetTour3D may be building the world's scene graph at the same time that TourWatcher is receiving detailsFor messages, so it is adding new branches to the same graph. It is (just about) possible that addVisitor( ) is called before the scene graph has been compiled (and made live) in createSceneGraph( ). This means Java 3D will be asked to add a branch (sBG) to a node (sceneBG) which is not yet live, causing the attachment to fail.


My solution is to delay the attachment by 200 ms, which solves the problem, at least in the many tests I've carried out.



Another thread-related problem of this type is when multiple threads attempt to add branches to the same live node simultaneously. This may cause one or more of the attachments to fail to become live. The solution is to add the synchronization code to the method doing the attachment, preventing multiple threads from executing it concurrently. Fortunately, this problem doesn't arise in NetTour3D since new branches are only added to a client by a single TourWatcher thread.










    No comments: