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:

[Download source]

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;
    }
}