The Source for Java Technology Collaboration


Home | Changes | Index | Search | Go

Extending Project Wonderland by Creating New Cell Types - Part V

by Jordan Slott (jslott@dev.java.net)

Purpose

In this tutorial, you will complete the simple new cell type you have created in Part 1, Part 2, Part 3, and Part 4 of this tutorial series by enabling it to synchronize its state among many Wonderland clients. You will modify your shape cell type to communicate when the user clicks on the shape to change its type back to the server-side object. The server-side object updates its state in a thread-safe manner, and communicates the change back to all of the clients.

This tutorial is designed for the v0.3 and v0.4 releases of Project Wonderland.

Expected Duration: 45 minutes

Prerequisites

Before completing this tutorial, you should have already successfully completed Part 1, Part 2, Part 3, and Part 4 of this tutorial series. You will be extending the functionality you implemented there.

You should also be familiar with programming Project Darkstar: it is the middleware technology upon which Project Wonderland is built that handles client-server communication and makes it easy to write server-side objects to manage their state in a thread-safe manner. Visit the Project Darkstar website--it is best to download the distribution and read the server-side tutorial document.

High-level Design

From a high-level, here is what you will implement in this tutorial to enable your cell type for a multi-user environment:

  1. Communicate the new shape type from the client-side ShapeCell class to the server-side ShapeCellGLO class when the user clicks on the shape.
  2. Update the shape type state stored by the ShapeCellGLO class.
  3. Communicate the new shape type to all other clients.

Creating a new message: ShapeCellChangeMessage

Before the client can communicate any changes in the type of shape when the user clicks on the shape, you need to create a new class to communicate this message.

In the org.jdesktop.lg3d.wonderland.shapecell.common package, create a new class named ShapeCellChangeMessage.java. Add the following import statements near the top of the file:

import java.nio.ByteBuffer;
import org.jdesktop.lg3d.wonderland.darkstar.common.CellID;
import org.jdesktop.lg3d.wonderland.darkstar.common.messages.CellMessage;
import org.jdesktop.lg3d.wonderland.darkstar.common.messages.DataString;

Next, have your class extend the CellMessage class as follows:

public class ShapeCellChangeMessage extends CellMessage {

The CellMessage class is the base class for all messages passed between the client and server cell classes in Project Wonderland. The two methods you will need to override from this class are extractMessageImpl() and populateDataElements(), but first you'll create some of the needed infrastructure for this class.

This message communicates the new shape of the cell to be displayed on all clients, so add a field to your class to store this new type (as a string), along with setter and getter methods:

    private String shapeType = null;

    public String getShapeType() {
        return this.shapeType;
    }

    public void setShapeType(String shapeType) {
        this.shapeType = shapeType;
    }

Next, implement the following constructor. As its first argument, it takes a CellID object: this is necessary so that when the message is received by the Wonderland server, it knows to which object it should dispatch the messsage. Each cell's ID is unique and is assigned automatically when it is created by the Wonderland system.

    public ShapeCellChangeMessage(CellID cellID, String shapeType) {
        super(cellID);
        this.shapeType = shapeType;
    }

Also add the default constructor:

    public ShapeCellChangeMessage() {
    }

Finally, you'll implement the populateDataElements() and extractMessageImpl() methods. These methods populate the message with the proper information and extract data from the message into an instance of the ShapeCellChangeMessage class, respectively. (Note: although you pack messages by hand, you may also simply use Java's object serialization too. Version 0.5 of Project Wonderland will move entirely to object serialization).

The populateDataElements() method simply places the string (encapsulated by a DataString object) into a collection of data elements stored by the CellMessage superclass:

    @Override
    protected void populateDataElements() {
        super.populateDataElements();
        dataElements.add(new DataString(this.shapeType));
    }

The extractMessageImpl() method does the opposite of the populateDataElements(): given a byte-buffer of the message data sent over the network, extract the shape type attribute, using the DataString object to extract a string value:

    @Override
    protected void extractMessageImpl(ByteBuffer data) {
        super.extractMessageImpl(data);
        this.shapeType = DataString.value(data);
    }
}

Communicating the Shape Change to the Server

Next, you will modify your ShapeCell class to send a message to the server when the user clicks on the shape to change its type. Each cell automatically has a communication channel opened between the client-side and server- side cell classes. All you need to do on the client-side is to create a new message and send it.

Locate the processEvent() method in your MouseButtonListener inner class in ShapeCell.java. Locate where you invoke the setup() method to change the shape type locally. After this method call, insert the following two lines to create a new ShapeCellChangeMessage object with the new shape type and send the message to the server:

                    ShapeCellChangeMessage msg = new ShapeCellChangeMessage(getCellID(), shapeType);
                    ChannelController.getController().sendMessage(msg);

This has the effect of immediately updating the shape type visually on the client, so that the user gets immediate feedback and then sends a message to the server with the new shape. The getCellID() method is defined by the Cell superclass and simply returns the unique ID of the cell that is automatically assigned to the cell when it is created.

You will also need to add the following import statements to near the top of your ShapeCell.java file:

import org.jdesktop.lg3d.wonderland.darkstar.client.ChannelController;
import org.jdesktop.lg3d.wonderland.darkstar.common.messages.CellMessage;
import org.jdesktop.lg3d.wonderland.darkstar.common.messages.Message;
import org.jdesktop.lg3d.wonderland.shapecell.common.ShapeCellChangeMessage;

Synchronizing the State of the Cell on the Server

Your server-side cell class, ShapeCellGLO, now needs to do the following:

  1. Receive the message from the client.
  2. Update its state based upon the new shape type sent from the client.
  3. Inform all other clients of the new shape type.

Fortunately, because Wonderland is built on top of Project Darkstar, synchronizing the state among many different concurrent clients is made easy. But first, set up your class to receive the message from the client. You will need to open the cell's communication channel, by overriding the openChannel() method in CellGLO. Fortunately, CellGLO also provides a convenience method, openDefaultChannel(), to open the channel meant for the client-server cell communication:

    @Override
    public void openChannel() {
        this.openDefaultChannel();
    }

Whenever a message is received, it calls the receivedMessage() method on the CellMessageListener interface. Add a stub for the receivedMessage() method, you will fill in its details shortly:

    public void receivedMessage(ClientSession client, CellMessage message) {

    }

You will also need to make your ShapeCellGLO class implement the CellMessageListener interface. Then, add the following imports to near the top of your file:

import org.jdesktop.lg3d.wonderland.darkstar.common.messages.CellMessage;
import org.jdesktop.lg3d.wonderland.darkstar.server.CellMessageListener;
import org.jdesktop.lg3d.wonderland.shapecell.common.ShapeCellChangeMessage;
import java.util.HashSet;
import java.util.Set;
import com.sun.sgs.app.ClientSession;

and modify the definition of ShapeCellGLO as follows:

public class ShapeCellGLO  extends StationaryCellGLO implements BeanSetupGLO, CellMessageListener

Next, you'll implement the details of the receivedMessage() method. It will accomplish two things: update the state of the shapeType member variable to reflect the new shape and communicate the new shape type to all of the clients. Since Project Wonderland is a multi-user environment, there may be several different users who click on their shape to update the shape type at the same time. In a typical multi-user environment, you must make sure updates to the state type is synchronized with all other possible updates. This is often a very tricky task!

Fortunately, the Project Darkstar infrastructure makes this really simple. All updates to the state of server-side objects happen within the context of a transaction -- either the state update happens at once without conflicting with other requests to update the state of the object, or it does not happen at all. The Project Darkstar infrastructure manages the resource contention for you: it notes what objects you update and when the transaction completes and commits its changes, it does so in an atomic fashion. It knows ShapeCellGLO is such an object to manage in a transaction because (in Part 1) you had it implement the ManagedObject interface by extending the StationaryCellGLO class.

When a message is delivered to your server-side cell class, it automatically happens within the context of a transaction: all you need to do is update the state you wish and when the method completes, Project Darkstar will commit any changes you made atomically and in a multi-user safe manner.

So, in your receivedMessage() method, the first two lines will be:

        ShapeCellChangeMessage sccm = (ShapeCellChangeMessage)message;
        this.shapeType = sccm.getShapeType();

These two lines are all that's required to insure that the state of this cell that is shared among many users is updated in a safe manner. Next, send a message to all of the other clients connected of the new state. (You do not need to send a message back to the client from which the change came -- although doing so would be harmless!). Note that you'll use the same ShapeCellChangeMessage class that you used to communicate the new shape type from the client to the server. In this case, you'll use the default constructor that sets the cell ID in CellMessage to null (you do not need it in this instance):

        ShapeCellChangeMessage msg = new ShapeCellChangeMessage();
        msg.setShapeType(shapeType);

And to actually go ahead and send the message:

        Set<ClientSession> sessions = new HashSet<ClientSession>(getCellChannel().getSessions());
        sessions.remove(client);
        getCellChannel().send(sessions, msg.getBytes());

The getCellChannel() method returns the communications channel associated with this cell. It returns a list of the Wonderland clients attached to the channel via the getSessions() method.

Listening for Message on the Client

There is one final step that completes the loop: your ShapeCell client-side class must also listen for messages sent to it that inform it of a new cell type. Here, your ShapeCell class must implement the ExtendedClientChannelListener interface and the setChannel(), leftChannel(), and receivedMessage() methods. First, modify the definition of ShapeCell as follows:

public class ShapeCell extends Cell implements ExtendedClientChannelListener {

and add the following import statements:

import org.jdesktop.lg3d.wonderland.darkstar.client.ExtendedClientChannelListener;
import com.sun.sgs.client.ClientChannel;
import com.sun.sgs.client.SessionId;

The ExtendedClientChannelListener interface comes with Project Wonderland and extends Darkstar's ClientChannelListener interface. The former adds the setChannel() method which you can implement simply as follows:

    public void setChannel(ClientChannel channel) {
        this.channel = channel;
    }

The this.channel member variable is part of the Cell superclass. The ClientChannelListener interface defines two methods you must implement: leftChannel() (which can be empty) and receivedMessage() that handles the ShapeCellChangeMessage message from the server:

    public void leftChannel(ClientChannel arg0) {
        // ignore
    }
   
    public void receivedMessage(ClientChannel client, SessionId session, byte[] data) {
        CellMessage msg = Message.extractMessage(data, CellMessage.class);
        if (msg instanceof ShapeCellChangeMessage) {
            ShapeCellChangeMessage sccm = Message.extractMessage(data, ShapeCellChangeMessage.class);
            this.shapeType = sccm.getShapeType();
            if (bg != null) {
                bg.detach();
            }
            this.setup();
        }
    }

The receivedMessage() method simply extracts a ShapeCellChangeMessage message from the message byte array, fetches the new shape type, sets the shapeType member variable to the new type, detaches the current shape from the scene, and asks the setup() method to draw the new shape.

Running Your New Shape Cell

To recompile and re-run both the Wonderland server and client, in two separate terminal windows:

% ant run-sgs
% ant run

You'll also want someone else on another machine to run the same client. (Note that you will both have to use the same client code -- it is best to setup the machine you run the Wonderland server to allow users to download the client software via Java Web Start).

Whenever you click on the box or sphere, it should change to the other shape type. The other user should see the shape change type too!

Summary

In this five part tutorial series, you learned how to extend Wonderland by creating a new cell type. You set up the project infrastructure, drew a basic object configured by a file in WFS, and then used the client side asset manager to load a texture for the shape. Finally, you learned how to make your new cell type synchronize its state among many Wonderland clients.

Topic ProjectWonderlandNewCellPart5 . { Edit | Ref-By | Printable | Diffs r5 < r4 < r3 < r2 < r1 | More }
 XML java.net RSS

Revision r5 - 16 Jul 2008 - 12:45:21 - Main.nicer94
Parents: WebHome > ProjectWonderland > ProjectWonderlandNewCell > ProjectWonderlandNewCellPart2 > ProjectWonderlandNewCellPart3 > ProjectWonderlandNewCellPart4