Multiuser Fridge Magnets
This example combines client-side Reactor code with a server-side room module to create a fridge-magnet application. When a user drags a letter, all other connected users see the letter move. Every 30 seconds, the server resets all letter positions. Here's the application:
In the fridge magnet application, letter positions are maintained by a server-side room module in a Java class:
net.user1.union.example.roommodule.FridgeMagnetsRoomModule
When a Reactor client connects, it asks to create the application room, and specifies the preceding class as a module for the room.
var modules:RoomModules = new RoomModules(); modules.addModule("net.user1.union.example.roommodule.FridgeMagnetsRoomModule", ModuleType.CLASS); room = reactor.getRoomManager().createRoom("examples.fridgemagnets", settings, null, modules);
Each "letter magnet" in the application is represented by a room attribute indicating the letter to display on the magnet and the magnet's (x, y) position. Magnet attributes are named sequentially, as in "magnet0", "magnet1", "magnet2". Each magnet attribute value is a comma-delimited string in the format "LETTER,X,Y". For example, here are the first five magnet attributes in the application, showing magnets in their default positions:
Attribute Name Attribute Value magnet0 A,140,25 magnet1 B,165,25 magnet2 C,190,25 magnet3 D,215,25 magnet4 E,240,25
Upon joining the magnet room, each client automatically receives the room's attributes. For each magnet attribute, the client creates and positions a magnet graphic, as shown in the following ActionScript code:
protected function joinListener (e:RoomEvent):void { // Get a hash of all room attributes var attributes:Object = room.getAttributes(); var magnetData:Array; var magnet:Magnet; // Initialize the magnets. Each attribute that begins with "magnet" // represents a magnet. for (var attributeName:String in attributes) { // If the attribute name begins with "magnet"... if (attributeName.indexOf("magnet") == 0) { // Convert the attribute's string value to an array magnetData = String(attributes[attributeName]).split(","); // Create a new magnet graphic, and add it to the magnets hash magnets[attributeName] = new Magnet(); magnet = magnets[attributeName]; magnet.name = attributeName; magnet.setLabel(magnetData[0]); magnet.x = parseInt(magnetData[1]); magnet.y = parseInt(magnetData[2]); magnet.addEventListener(MouseEvent.MOUSE_DOWN, magnetMouseDownListener); magnet.addEventListener(MouseEvent.MOUSE_UP, magnetMouseUpListener); addChild(magnet); } } }
When a user drags one of the magnets, the Reactor client sends a module message named "MOVE" to the server-side room module. The message's arguments specify the client's desired new position for the magnet.
var moduleArgs:Object = new Object(); moduleArgs.MAGNET = Magnet(e.target).name; moduleArgs.X = Math.floor(Magnet(e.target).x).toString(); moduleArgs.Y = Math.floor(Magnet(e.target).y).toString(); room.sendModuleMessage("MOVE", moduleArgs);
When the room module receives a MOVE message it parses the new position of the magnet, and, if that position is legal, sets the corresponding room attribute to the new position. In the following code, notice that the magnet attribute is flagged as "server only," thus preventing malicious clients from changing the attribute value to an invalid position.
public void onModuleMessage(RoomEvent evt) { Message msg = evt.getMessage(); // --- if a move letter message then place the letter if ("MOVE".equals(msg.getMessageName())) { // --- get letter index and set the attribute try { int magnet = Integer.parseInt(msg.getArg("MAGNET").substring(6)); int x = Integer.parseInt(msg.getArg("X")); int y = Integer.parseInt(msg.getArg("Y")); if ((x >= 0 && x <= 600) && (y >= 0 && y <= 400)) { m_ctx.getRoom().setAttribute(msg.getArg("MAGNET"), m_letterPool[magnet]+","+msg.getArg("X")+","+ msg.getArg("Y"), Attribute.SCOPE_GLOBAL, Attribute.FLAG_SHARED | Attribute.FLAG_SERVER_ONLY); } } catch (NumberFormatException e) { e.printStackTrace(); } catch (AttributeException e) { e.printStackTrace(); } } }
The Code
Here is UnionFridgeMagnets, the main ActionScript class for the fridge magnets Flash client application:
package { import flash.display.Sprite; import flash.events.MouseEvent; import flash.text.TextField; import flash.text.TextFormat; import net.user1.logger.Logger; import net.user1.reactor.AttributeEvent; import net.user1.reactor.Reactor; import net.user1.reactor.ReactorEvent; import net.user1.reactor.Room; import net.user1.reactor.RoomEvent; import net.user1.reactor.ModuleType; import net.user1.reactor.RoomModules; import net.user1.reactor.RoomSettings; public class UnionFridgeMagnets extends Sprite { // A hash of magnet name, graphic pairs protected var magnets:Object; // The magnet room protected var room:Room; // The core Reactor object that connects to Union Server protected var reactor:Reactor; // A text field in which to display the number of users protected var output:TextField; // Constructor public function UnionFridgeMagnets () { // Make the magnets hash magnets = new Object(); // Make the output text field output = new TextField(); output.height = 20; output.width = stage.stageWidth; output.x = 10; output.y = 10; output.textColor = 0xFFFFFF; addChild(output); // Make the Reactor object and connect to Union Server reactor = new Reactor("config.xml"); reactor.addEventListener(ReactorEvent.READY, readyListener); } // Triggered when the connection is established and ready for use protected function readyListener (e:ReactorEvent):void { // Specify the server side room module for the fridge magnet room var modules:RoomModules = new RoomModules(); modules.addModule("net.user1.union.example.roommodule.FridgeMagnetsRoomModule", ModuleType.CLASS); // Keep the fridge magnet room alive forever var settings:RoomSettings = new RoomSettings(); settings.removeOnEmpty = false; // Create the fridge magnet room room = reactor.getRoomManager().createRoom("examples.fridgemagnets", settings, null, modules); // Register to be notified when this client joins the room room.addEventListener(RoomEvent.JOIN, joinListener); // Register to be notified when the number of room occupants changes room.addEventListener(RoomEvent.OCCUPANT_COUNT, occupantCountListener); // Registered to be notified when the room's attributes change room.addEventListener(AttributeEvent.UPDATE, attributeUpdateListener); // Join the room room.join(); } // Triggered when his client joins the room protected function joinListener (e:RoomEvent):void { // Get a hash of all room attributes var attributes:Object = room.getAttributes(); var magnetData:Array; var magnet:Magnet; // Initialize the magnets. Each attribute that begins with "magnet" // represents a magnet. for (var attributeName:String in attributes) { // If the attribute name begins with "magnet"... if (attributeName.indexOf("magnet") == 0) { // Convert the attribute's string value to an array magnetData = String(attributes[attributeName]).split(","); // Create a new magnet graphic, and add it to the magnets hash magnets[attributeName] = new Magnet(); magnet = magnets[attributeName]; magnet.name = attributeName; magnet.setLabel(magnetData[0]); magnet.x = parseInt(magnetData[1]); magnet.y = parseInt(magnetData[2]); magnet.addEventListener(MouseEvent.MOUSE_DOWN, magnetMouseDownListener); magnet.addEventListener(MouseEvent.MOUSE_UP, magnetMouseUpListener); addChild(magnet); } } } // Triggered when one of the room's attributes changes protected function attributeUpdateListener (e:AttributeEvent):void { var magnetData:Array; var magnet:Magnet; // If the changed attribute's name begins with "magnet" if (e.getChangedAttr().name.indexOf("magnet") == 0) { // If a magnet by the specified name exists in the magnets hash... magnet = magnets[e.getChangedAttr().name]; if (magnet != null) { // The magnet exists. Check if it is being dragged. If not, // move it to the location specified by the room attribute value. if (!magnet.isDragging()) { magnetData = e.getChangedAttr().value.split(","); magnet.x = parseInt(magnetData[1]); magnet.y = parseInt(magnetData[2]); } } } } // Triggered when the room occupant count changes protected function occupantCountListener (e:RoomEvent):void { output.text = "Users connected: " + e.getNumClients(); } // Triggered when a magnet is dragged. protected function magnetMouseDownListener (e:MouseEvent):void { // Move the magnet being dragged in front of all other magnets setChildIndex(Magnet(e.target), this.numChildren - 1); } // Triggered when a magnet is released. protected function magnetMouseUpListener (e:MouseEvent):void { // The magnet was released, so send the moved magnet's new // position to the server. Send the position in a module message // rather than assigning the room attribute directly so that // the server can decide whether the requested move is legal before // allowing the magnet to be repositioned. If the move request is // granted, the server will set the room attribute, which will trigger // attributeUpdateListener(). var moduleArgs:Object = new Object(); moduleArgs.MAGNET = Magnet(e.target).name; moduleArgs.X = Math.floor(Magnet(e.target).x).toString(); moduleArgs.Y = Math.floor(Magnet(e.target).y).toString(); room.sendModuleMessage("MOVE", moduleArgs); } } }
Here is the client-side Magnet class, which represents a magnet graphic in the application.
package { import flash.display.Sprite; import flash.events.MouseEvent; import flash.text.TextField; import flash.text.TextFieldAutoSize; // A magnet graphic public class Magnet extends Sprite { protected var label:TextField; protected var dragging:Boolean; public function Magnet () { mouseChildren = false; addEventListener(MouseEvent.MOUSE_DOWN, magnetMouseDownListener); addEventListener(MouseEvent.MOUSE_UP, magnetMouseUpListener, false, int.MAX_VALUE); label = new TextField(); label.background = true; label.border = true; label.width = 20; label.height = 20; label.selectable = false; addChild(label); } public function setLabel (value:String):void { label.text = value; } public function isDragging ():Boolean { return dragging; } protected function magnetMouseDownListener (e:MouseEvent):void { startDrag(); dragging = true; } protected function magnetMouseUpListener (e:MouseEvent):void { stopDrag(); dragging = false; } } }
Here is the config.xml file loaded by the Flash client.
<?xml version="1.0"?> <config> <connections> <connection host="tryunion.com" port="80"/> </connections> <logLevel>INFO</logLevel> </config>
Here is the complete Java code for the fridge magnets room module.
package net.user1.union.example.roommodule; import net.user1.union.api.Message; import net.user1.union.api.Module; import net.user1.union.core.attribute.Attribute; import net.user1.union.core.context.ModuleContext; import net.user1.union.core.event.RoomEvent; import net.user1.union.core.exception.AttributeException; /** * This is the RoomModule that controls the fridge magnets game. The fridge * magnet Reactor (Flash) client requests that the module be attached when it * sends the CREATE_ROOM (u24) UPC request. The server will create a new * instance of this module for each fridge magnets room. */ public class FridgeMagnetsRoomModule implements Module, Runnable { // --- the module context // --- use this to get access to the server and the room this module // --- is attached to private ModuleContext m_ctx; // --- the thread for the fridge magnet app private Thread m_thread; // --- letter pool String[] m_letterPool = new String[] {"A","B","C","D","E","F","G","H","I", "J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"}; /** * The init method is called when the instance is created. */ public boolean init(ModuleContext ctx) { m_ctx = ctx; // --- initialize our letter attributes resetLetters(); // --- create the app thread and start it m_thread = new Thread(this); m_thread.start(); // --- register to receive room module messages // --- the onModuleMessage method will be called whenever a // --- room module message (u70) is sent to the room m_ctx.getRoom().addEventListener(RoomEvent.MODULE_MESSAGE, this, "onModuleMessage"); // --- the module initialized fine return true; } /** * The main game loop. Reset the letters every 30 seconds. */ public void run() { // --- while the room module is running while (m_thread != null) { // --- reset the letters resetLetters(); // --- pause to let clients move the letters around for a bit // --- before resetting them try { Thread.sleep(30000L); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * Reset the letters to their original position. */ private void resetLetters() { // --- reset the position of the letters for (int i=0;i<m_letterPool.length;i++) { // --- set a room scoped attribute for the letter try { m_ctx.getRoom().setAttribute("magnet"+i, m_letterPool[i]+","+(140+(i%13)*25)+","+((i/13+1)*25), Attribute.SCOPE_GLOBAL, Attribute.FLAG_SHARED | Attribute.FLAG_SERVER_ONLY); } catch (AttributeException e) { e.printStackTrace(); } } } /** * Called when the room receives a room module message. * * @param evt The RoomEvent contianing information about the event. */ public void onModuleMessage(RoomEvent evt) { Message msg = evt.getMessage(); // --- if a move letter message then place the letter if ("MOVE".equals(msg.getMessageName())) { // --- get letter index and set the attribute try { int magnet = Integer.parseInt(msg.getArg("MAGNET").substring(6)); int x = Integer.parseInt(msg.getArg("X")); int y = Integer.parseInt(msg.getArg("Y")); if ((x >= 0 && x <= 600) && (y >= 0 && y <= 400)) { m_ctx.getRoom().setAttribute(msg.getArg("MAGNET"), m_letterPool[magnet]+","+msg.getArg("X")+","+ msg.getArg("Y"), Attribute.SCOPE_GLOBAL, Attribute.FLAG_SHARED | Attribute.FLAG_SERVER_ONLY); } } catch (NumberFormatException e) { e.printStackTrace(); } catch (AttributeException e) { e.printStackTrace(); } } } /** * The shutdown method is called when the server removes the room which * also removes the room module. */ public void shutdown() { // --- deregister for module messages m_ctx.getRoom().removeEventListener(RoomEvent.MODULE_MESSAGE, this, "onModuleMessage"); m_thread = null; } }