 |
| |
| META TOPICPARENT | name="ProjectWonderlandNewCellPart4" |
Home | Changes | Index | Search | Go <-- This creates the navigation links to : Home | Help | Index | etc. -->
Extending Project Wonderland by Creating New Cell Types - Part V
by Jordan Slott (jslott@dev.java.net) | |
< < | UNDER CONSTRUCTION -- CHECK BACK SOON! | | | <-- Your JavaDesktop? article goes here. Please try to include at least one sentence describing this topic. -->
<-- Also please try to include at least one sentence describing where each link goes. -->
<-- Please make sure some other page points to your new article so that others can find it! --> | | | 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 type of class to communicate | > > | 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 | | | 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. Add the following imports to near the top of your file that you will need later:
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;
| > > | 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: | | |
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 your the Wonderland server, it knows to which object it should dispatch the messsage. Each cell's ID is unique and is assigned | > > | 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. | | |
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. (Note: although | > > | 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 extractMessageImpl() method does the opposite of the populateDataElements(): give a byte-buffer of the message data send over | > > | 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: | | | Communicating the Shape Change to the Server | |
< < | Next, you will modify you ShapeCell class to send a message to the server when the user clicks on the shape to | > > | 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. | | | 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 communicate channel, by overriding the openChannel() method in CellGLO. Fortunately, | > > | 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: | | | 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 | |
< < | trick task! | > > | tricky task! | | | | |
< < | Fortunately, the Project Darkstar infrastructure makes this really simply. All updates to the state of server-side | > > | 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 as object to manage | > > | 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. |
| |
| META TOPICPARENT | name="ProjectWonderlandNewCellPart4" |
Home | Changes | Index | Search | Go <-- This creates the navigation links to : Home | Help | Index | etc. --> | | | Purpose | |
< < | In this tutorial, you will enhance the simple new cell type you have created in Part 1,
Part 2, and Part 3 of this tutorial series by
registering a listener to receive mouse button events. You will modify your shape cell type to switch between
displaying a sphere and cube whenever the user clicks on your cell. | > > | 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: 30 minutes | > > | Expected Duration: 45 minutes | | | Prerequisites
Before completing this tutorial, you should have already successfully completed Part 1, | |
< < | Part 2, and Part 3 of this tutorial series. You will
be extending the functionality you implemented there. | > > | Part 2, Part 3, and Part 4
of this tutorial series. You will be extending the functionality you implemented there. | | | | |
< < | Handling user input events in ShapeCell.java | > > | 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. | | | | |
< < | In this tutorial, your modifications will occur entirely in the ShapeCell.java file. You should use your existing code from
previous tutorials as a base, even though you'll be gutting several of the methods in there. Conceptually, the changes are
straightforward: you'll register for mouse events and when the user clicks the mouse button, you will render a sphere if the
current shape is a cube, and visa versa. | > > | High-level Design | | | | |
< < | To start, add the following import statements to the top of your file. (Although it is always good at the end to have your IDE
fix all of your import statements for you): | > > | From a high-level, here is what you will implement in this tutorial to enable your cell type for a multi-user environment:
- Communicate the new shape type from the client-side ShapeCell class to the server-side ShapeCellGLO class when the user clicks on the shape.
- Update the shape type state stored by the ShapeCellGLO class.
- 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 type of 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 org.jdesktop.lg3d.wg.event.LgEvent;
import org.jdesktop.lg3d.wg.event.LgEventListener;
import org.jdesktop.lg3d.wg.event.MouseButtonEvent3D;
import org.jdesktop.lg3d.wg.internal.j3d.j3dnodes.J3dLgBranchGroup; | > > | 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, make the following modifications to the class member variable declarations at the top of your class definition: | > > | Next, have your class extend the CellMessage class as follows: | | | | |
< < | J3dLgBranchGroup? bg = null;
private URL baseURL = null;
private String shapeType = null;
private String texturePath = null; | > > | public class ShapeCellChangeMessage? extends CellMessage? { | | | | |
< < | Note that the type of the variable bg changed from BranchGroup to J3dLgBranchGroup. The J3dLgBranchGroup
object comes as part of the Looking Glass 3D platform upon which Wonderland is built. This
change is needed to capture keyboard and mouse events. | > > | 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. Add the following imports to near the top of your file that you will need later: | | | | |
< < | Also, you added class member variables to store several properties passed from the server upon creating (or reconfiguring)
the cell: the base URL for artwork assets, the relative path of the shape texture, and the type of the shape to create. | > > |
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, you'll rework the setup() method quite a bit. The changes are (mostly) relatively minor, but it is probably best to
completely replace your existing code for this method. First: | > > | 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: | | | | |
< < | @Override
public void setup(CellSetup? setupData) { | > > | private String shapeType = null; | | | | |
< < | String url = ((ShapeCellSetup?) setupData).getBaseURL();
try {
this.baseURL = new URL(url);
} catch (MalformedURLException? ex) {
System.err.println("Failed to load slide, bad baseURL " + url);
return; | > > | public String getShapeType() {
return this.shapeType; | | | } | |
< < | this.texturePath = new String(((ShapeCellSetup?)setupData).getTexturePath());
this.shapeType = new String(((ShapeCellSetup?)setupData).getShapeType()); | | | | |
< < | this.setup(); | > > | public void setShapeType(String shapeType) {
this.shapeType = shapeType; | | | }
| |
< < | This setup() method simply takes the information passed from the server via the ShapeCellSetup object
and sets the values of the class member variables. Note that it calls another setup() method (which takes
no arguments) before it returns. Implement this new setup() method as follows: | > > | 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 your 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 void setup() {
bg = new J3dLgBranchGroup?(); | > > | public ShapeCellChangeMessage?(CellID? cellID, String shapeType) {
super(cellID);
this.shapeType = shapeType;
}
| | | | |
< < | bg.setCapabilities();
bg.setMouseEventEnabled(true);
bg.setMouseEventSource(MouseButtonEvent3D?.class, true);
bg.addListener(new MouseButtonListener?()); | > > | Also add the default constructor: | | | | |
< < | bg.setCapability(BranchGroup?.ALLOW_CHILDREN_EXTEND);
bg.setCapability(BranchGroup?.ALLOW_CHILDREN_WRITE);
SceneGraphUtil?.setCapabilitiesGraph(bg, false); | > > |
public ShapeCellChangeMessage() {
}
| | | | |
< < | ImageComponent2DURL? ic = AssetManager?.getAssetManager().createImageComponent2DURL(baseURL, this.texturePath); | > > | 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. (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). | | | | |
< < | Appearance app = new Appearance();
Texture2D? tex = new Texture2D?(Texture.BASE_LEVEL, Texture.RGBA, ic.getWidth(), ic.getHeight());
tex.setImage(0, ic);
app.setTexture(tex); | > > | The populateDataElements() method simply places the string (encapsulated by a DataString object) into a collection of data elements
stored by the CellMessage superclass: | | | | |
< < | if (this.shapeType.compareTo("BOX") == 0) {
Box box = new Box(1, 1, 1, Box.GENERATE_TEXTURE_COORDS, app);
SceneGraphUtil?.setCapabilitiesGraph(box, false);
bg.addChild(box); | > > |
@Override
protected void populateDataElements() {
super.populateDataElements();
dataElements.add(new DataString(this.shapeType)); | | | } | |
< < | else if (this.shapeType.compareTo("SPHERE") == 0) {
Sphere sphere = new Sphere(1, Sphere.GENERATE_TEXTURE_COORDS, app);
SceneGraphUtil?.setCapabilitiesGraph(sphere, false);
bg.addChild(sphere); | > > |
The extractMessageImpl() method does the opposite of the populateDataElements(): give a byte-buffer of the message data send 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); | | | } | |
< < | cellLocal.addChild(bg); | | | }
| |
< < | This new setup() method is similar to the setup() method you defined in earlier tutorials with
a couple of differences. First, it creates an J3dLgBranchGroup object instead of a BranchGroup
object. It also uses the values for the base asset URL, relative texture path, and shape type stored
in the class member variables. This is done so that (as shown below) you can change the value
of shapeType and have this setup() method redraw your scene. | > > | Communicating the Shape Change to the Server | | | | |
< < | The most important change is the addition of the following lines of code near the top of the method: | > > | Next, you will modify you 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: | | | | |
< < | bg.setCapabilities();
bg.setMouseEventEnabled(true);
bg.setMouseEventSource(MouseButtonEvent3D?.class, true);
bg.addListener(new MouseButtonListener?()); | > > | ShapeCellChangeMessage? msg = new ShapeCellChangeMessage?(getCellID(), shapeType);
ChannelController?.getController().sendMessage(msg); | | | | |
< < | These four lines of code tell the J3dLgBranchGroup object that you want to receive any mouse
button events that happen inside of the cell. Although not explicitly covered in this tutorial, listening for
keyboard events is also done in a similar way. Consult the J3dLGBranchGroup Javadoc
for more information. Your listener class, MouseButtonListener, which you define below, receives
the mouse events. | > > | 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. | | | | |
< < | Note: You may leave the existing reconfigure() method as defined in previous tutorials as-is. | > > | You will also need to add the following import statements to near the top of your ShapeCell.java file: | | | | |
< < | Implementing the mouse listener | > > |
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;
| | | | |
< < | The remaining step is to implement the class that will handle the mouse events, MouseButtonListener.
You can make this class an inner class of the ShapeCell class, to save you from passing some needed
information between the two. At the end of the ShapeCell class (but still within the class definition), add: | > > | Synchronizing the State of the Cell on the Server
Your server-side cell class, ShapeCellGLO, now needs to do the following:
- Receive the message from the client.
- Update its state based upon the new shape type sent from the client.
- 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 communicate 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: | | | | |
< < | class MouseButtonListener? implements LgEventListener? { | > > | @Override
public void openChannel() {
this.openDefaultChannel();
}
| | | | |
< < | /** Default constructor */
public MouseButtonListener?() {} | > > | 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 processEvent(LgEvent? evt) {
if (evt instanceof MouseButtonEvent3D?) {
MouseButtonEvent3D? mbEvent = (MouseButtonEvent3D?)evt;
if (mbEvent.isClicked() == true) {
System.out.println("MOUSE BUTTON EVENT: " + evt.toString()); | > > |
public void receivedMessage(ClientSession client, CellMessage message) { | | | | |
< < | shapeType = (shapeType.compareTo("BOX") == 0) ? "SPHERE" : "BOX";
if (bg = null) {
bg.detach();
}
setup(); | | | } | |
> > |
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;
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
trick task!
Fortunately, the Project Darkstar infrastructure makes this really simply. 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 as 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 statement:
import org.jdesktop.lg3d.wonderland.darkstar.client.ExtendedClientChannelListener;
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 | | | } | |
< < | @SuppressWarnings("unchecked")
public Class[] getTargetEventClasses() {
return new Class[] { MouseButtonEvent3D?.class }; | > > | 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(); | | | }
}
| |
< < | It is the processEvent() method that actually handles the mouse button event. The mouse button event is represented
by MouseButtonEvent3D, a subclass of LgEvent. After it checks that the event you have received is indeed a mouse
button event, the processEvent() method checks if the mouse button was clicked (your listener will also receive events
for mouse button presses and releases, which you ignore). If the mouse button was clicked, you then change the shape
drawn in the cell, detach the current shape's branch group from the scene, and ask the setup() method to redraw the
shape in the scene.
Finally, the getTargetEventClasses() method simply returns an array of event classes that your listener is interested in--
in this case, MouseButtonEvent3D. | > > | 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 | | | % ant run
| |
< < | Whenever you click on the box or sphere, it should change to the other shape type. Note that you should
keep a steady hand when clicking the mouse button. If you move the mouse even slightly, then a mouse
button click event is not registered!
Next Steps
The code you wrote for this tutorial does have a flaw: any time you click on the shape to change the type
of shape drawn, only you can see the new shape. Other avatars in the world looking at the shape will not
see your changes. In order for this to be done, you must communicate the shape change to all other users
via the Wonderland server, making sure that all users see the same shape. In the final tutorial in this
series, you will learn how to communicate with the Wonderland server and synchronize the state of your
cell among many users. | | | \ No newline at end of file | |
> > | 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. |
|
> > |
| META TOPICPARENT | name="ProjectWonderlandNewCellPart4" |
Home | Changes | Index | Search | Go <-- This creates the navigation links to : Home | Help | Index | etc. -->
Extending Project Wonderland by Creating New Cell Types - Part V
by Jordan Slott (jslott@dev.java.net)
UNDER CONSTRUCTION -- CHECK BACK SOON!
<-- Your JavaDesktop? article goes here. Please try to include at least one sentence describing this topic. -->
<-- Also please try to include at least one sentence describing where each link goes. -->
<-- Please make sure some other page points to your new article so that others can find it! -->
<-- For more on how to write Javapedia articles please read the WritingArticles? page. -->
Purpose
In this tutorial, you will enhance the simple new cell type you have created in Part 1,
Part 2, and Part 3 of this tutorial series by
registering a listener to receive mouse button events. You will modify your shape cell type to switch between
displaying a sphere and cube whenever the user clicks on your cell.
This tutorial is designed for the v0.3 and v0.4 releases of Project Wonderland.
Expected Duration: 30 minutes
Prerequisites
Before completing this tutorial, you should have already successfully completed Part 1,
Part 2, and Part 3 of this tutorial series. You will
be extending the functionality you implemented there.
Handling user input events in ShapeCell.java
In this tutorial, your modifications will occur entirely in the ShapeCell.java file. You should use your existing code from
previous tutorials as a base, even though you'll be gutting several of the methods in there. Conceptually, the changes are
straightforward: you'll register for mouse events and when the user clicks the mouse button, you will render a sphere if the
current shape is a cube, and visa versa.
To start, add the following import statements to the top of your file. (Although it is always good at the end to have your IDE
fix all of your import statements for you):
import org.jdesktop.lg3d.wg.event.LgEvent;
import org.jdesktop.lg3d.wg.event.LgEventListener;
import org.jdesktop.lg3d.wg.event.MouseButtonEvent3D;
import org.jdesktop.lg3d.wg.internal.j3d.j3dnodes.J3dLgBranchGroup;
Next, make the following modifications to the class member variable declarations at the top of your class definition:
J3dLgBranchGroup bg = null;
private URL baseURL = null;
private String shapeType = null;
private String texturePath = null;
Note that the type of the variable bg changed from BranchGroup to J3dLgBranchGroup. The J3dLgBranchGroup
object comes as part of the Looking Glass 3D platform upon which Wonderland is built. This
change is needed to capture keyboard and mouse events.
Also, you added class member variables to store several properties passed from the server upon creating (or reconfiguring)
the cell: the base URL for artwork assets, the relative path of the shape texture, and the type of the shape to create.
Next, you'll rework the setup() method quite a bit. The changes are (mostly) relatively minor, but it is probably best to
completely replace your existing code for this method. First:
@Override
public void setup(CellSetup setupData) {
String url = ((ShapeCellSetup) setupData).getBaseURL();
try {
this.baseURL = new URL(url);
} catch (MalformedURLException ex) {
System.err.println("Failed to load slide, bad baseURL " + url);
return;
}
this.texturePath = new String(((ShapeCellSetup)setupData).getTexturePath());
this.shapeType = new String(((ShapeCellSetup)setupData).getShapeType());
this.setup();
}
This setup() method simply takes the information passed from the server via the ShapeCellSetup object
and sets the values of the class member variables. Note that it calls another setup() method (which takes
no arguments) before it returns. Implement this new setup() method as follows:
public void setup() {
bg = new J3dLgBranchGroup();
bg.setCapabilities();
bg.setMouseEventEnabled(true);
bg.setMouseEventSource(MouseButtonEvent3D.class, true);
bg.addListener(new MouseButtonListener());
bg.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND);
bg.setCapability(BranchGroup.ALLOW_CHILDREN_WRITE);
SceneGraphUtil.setCapabilitiesGraph(bg, false);
ImageComponent2DURL ic = AssetManager.getAssetManager().createImageComponent2DURL(baseURL, this.texturePath);
Appearance app = new Appearance();
Texture2D tex = new Texture2D(Texture.BASE_LEVEL, Texture.RGBA, ic.getWidth(), ic.getHeight());
tex.setImage(0, ic);
app.setTexture(tex);
if (this.shapeType.compareTo("BOX") == 0) {
Box box = new Box(1, 1, 1, Box.GENERATE_TEXTURE_COORDS, app);
SceneGraphUtil.setCapabilitiesGraph(box, false);
bg.addChild(box);
}
else if (this.shapeType.compareTo("SPHERE") == 0) {
Sphere sphere = new Sphere(1, Sphere.GENERATE_TEXTURE_COORDS, app);
SceneGraphUtil.setCapabilitiesGraph(sphere, false);
bg.addChild(sphere);
}
cellLocal.addChild(bg);
}
This new setup() method is similar to the setup() method you defined in earlier tutorials with
a couple of differences. First, it creates an J3dLgBranchGroup object instead of a BranchGroup
object. It also uses the values for the base asset URL, relative texture path, and shape type stored
in the class member variables. This is done so that (as shown below) you can change the value
of shapeType and have this setup() method redraw your scene.
The most important change is the addition of the following lines of code near the top of the method:
bg.setCapabilities();
bg.setMouseEventEnabled(true);
bg.setMouseEventSource(MouseButtonEvent3D.class, true);
bg.addListener(new MouseButtonListener());
These four lines of code tell the J3dLgBranchGroup object that you want to receive any mouse
button events that happen inside of the cell. Although not explicitly covered in this tutorial, listening for
keyboard events is also done in a similar way. Consult the J3dLGBranchGroup Javadoc
for more information. Your listener class, MouseButtonListener, which you define below, receives
the mouse events.
Note: You may leave the existing reconfigure() method as defined in previous tutorials as-is.
Implementing the mouse listener
The remaining step is to implement the class that will handle the mouse events, MouseButtonListener.
You can make this class an inner class of the ShapeCell class, to save you from passing some needed
information between the two. At the end of the ShapeCell class (but still within the class definition), add:
class MouseButtonListener implements LgEventListener {
/** Default constructor */
public MouseButtonListener() {}
public void processEvent(LgEvent evt) {
if (evt instanceof MouseButtonEvent3D) {
MouseButtonEvent3D mbEvent = (MouseButtonEvent3D)evt;
if (mbEvent.isClicked() == true) {
System.out.println("MOUSE BUTTON EVENT: " + evt.toString());
shapeType = (shapeType.compareTo("BOX") == 0) ? "SPHERE" : "BOX";
if (bg != null) {
bg.detach();
}
setup();
}
}
}
@SuppressWarnings("unchecked")
public Class<LgEvent>[] getTargetEventClasses() {
return new Class[] { MouseButtonEvent3D.class };
}
}
It is the processEvent() method that actually handles the mouse button event. The mouse button event is represented
by MouseButtonEvent3D, a subclass of LgEvent. After it checks that the event you have received is indeed a mouse
button event, the processEvent() method checks if the mouse button was clicked (your listener will also receive events
for mouse button presses and releases, which you ignore). If the mouse button was clicked, you then change the shape
drawn in the cell, detach the current shape's branch group from the scene, and ask the setup() method to redraw the
shape in the scene.
Finally, the getTargetEventClasses() method simply returns an array of event classes that your listener is interested in--
in this case, MouseButtonEvent3D.
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
Whenever you click on the box or sphere, it should change to the other shape type. Note that you should
keep a steady hand when clicking the mouse button. If you move the mouse even slightly, then a mouse
button click event is not registered!
Next Steps
The code you wrote for this tutorial does have a flaw: any time you click on the shape to change the type
of shape drawn, only you can see the new shape. Other avatars in the world looking at the shape will not
see your changes. In order for this to be done, you must communicate the shape change to all other users
via the Wonderland server, making sure that all users see the same shape. In the final tutorial in this
series, you will learn how to communicate with the Wonderland server and synchronize the state of your
cell among many users. |
|