Online Multiplayer Pong!

[Play Union Pong | Download source]

Overview

Union Pong consists of a server-side room module written in Java, and a Flash client-side application written in pure ActionScript with Union's Reactor framework. The room module is responsible for controlling the game's flow, scoring, and physics simulation. It resides in a single Java class,

net.user1.union.example.pong.PongRoomModule

The Flash client displays an extrapolated version of the game based on updates from the server. It includes 14 class files, listed at the end of this article.

Running Union Pong

To run Union Pong on your own server, follow these steps:

  1. In Union Server's root directory, copy /examples/union_examples.jar to /modules/union_examples.jar.
  2. Compile a .swf file from the ActionScript source included in the Union Pong source archive.
  3. Copy the file /config/config.xml into the the same directory as the .swf file from Step 2.
  4. Change the host and port listed in config.xml to your server's host and port.
  5. Run the .swf file.

Code Walkthrough

When a Pong client connects, it asks to create the Pong game room, and specifies PongRoomModule as a module for the room.

var modules:RoomModules = new RoomModules();
modules.addModule("net.user1.union.example.pong.PongRoomModule",
                  ModuleType.CLASS);
var room:Room = reactor.getRoomManager().createRoom(Settings.GAME_ROOMID,
                                                    settings,
                                                    null,
                                                    modules);

Next, the client joins the game room:

room.join();

To receive notification when a client joins the pong room, the room module registers for the server-side RoomEvent.ADD_CLIENT event.

        m_ctx.getRoom().addEventListener(RoomEvent.ADD_CLIENT, this, "onAddClient");

Upon joining the room, the client is initialized by the server-side room module. The module automatically assigns the client to either the left or right paddle by setting a client attribute, "side".

m_leftPlayer.setAttribute(ATTR_SIDE, "left",
  m_ctx.getRoom().getQualifiedID(), Attribute.FLAG_SERVER_ONLY | Attribute.FLAG_SHARED);

Then the module informs the client that it is ready to play by setting another client attribute, "status", to "ready":

m_leftPlayer.setAttribute(ATTR_STATUS, "ready",
  m_ctx.getRoom().getQualifiedID(), Attribute.FLAG_SERVER_ONLY | Attribute.FLAG_SHARED);

When two players have a "ready" status, the server-side room module sends a message named "START_GAME" to the game room.

evt.getRoom().sendMessage("START_GAME");

Each client listens for the "START_GAME" message with the following message listener:

protected function startGameListener (fromClient:IClient):void {
  lastUpdate = getTimer();
  hud.setStatus("");
  resetBall();
  court.showBall();
  hud.resetScores();
  state = GameStates.IN_GAME;
}

Upon receiving the "START_GAME" message, each Pong client begins a local simulation of the physical movements of the ball and paddle. Meanwhile, the server begins its own physics simulation that mirrors the ones being executed by the two Pong clients.

When a player presses either the up or down arrow key, the client application animates the paddle graphic, and also sets a client attribute called "paddle". The "paddle" attribute transmits the paddle's new position, speed, and direction to both the server and the opponent client.

setAttribute(ClientAttributes.PADDLE,
  paddle.x + "," + paddle.y + "," + paddle.speed + "," + paddle.direction,
  Settings.GAME_ROOMID);

In the following diagram, notice that the paddle attribute update is sent independently to the pong room module and to the opponent client.

The server and the client each independently register an event listener that is triggered when a player's "paddle" attribute changes.
The server's paddle listener updates the trajectory of the player's paddle in the server-side Pong physics simulation, which is the authoritative simulation that determines scoring. The client-side paddle listener updates the trajectory of the player's paddle in the client-side Pong physics simulation, which is used for strictly visual purposes only. The client-side display of the player paddles is an approximation of the actual real world on the server, and is not used to determine scoring.

Here's the server's "paddle"-attribute event listener:

public void onClientAttributeChanged(ClientEvent evt) {
  // --- was the attribute scoped to this room and for the paddle?
  if (evt.getAttribute().getScope().equals(m_ctx.getRoom().getQualifiedID())
      && evt.getAttribute().getName().equals(ATTR_PADDLE)) {
    // --- then update the paddle object
    PongObject paddle = null;
    String[] paddleAttrs = evt.getAttribute().nullSafeGetValue().split(",");
    if (evt.getClient().equals(m_leftPlayer)) {
      paddle = m_leftPaddle;
    } else if (evt.getClient().equals(m_rightPlayer)) {
      paddle = m_rightPaddle;
    }

    // --- parse the attribute and set the paddle
    if (paddle != null) {
      paddle.setX(Float.parseFloat(paddleAttrs[0]));
      paddle.setY(Float.parseFloat(paddleAttrs[1]));
      paddle.setSpeed(Integer.parseInt(paddleAttrs[2]));
      paddle.setDirection(Float.parseFloat(paddleAttrs[3]));
    }
  }
}

Here's the corresponding client-side attribute-change listener. It updates the trajectory of the client's opponent's paddle:

public function updateAttributeListener (e:AttributeEvent):void {
  if (e.getChangedAttr().name == ClientAttributes.PADDLE
      && e.getChangedAttr().scope == Settings.GAME_ROOMID
      && e.getChangedAttr().byClient == null) {
    deserializePaddle(e.getChangedAttr().value);
  }
}

As the ball moves around the playing field, the clients and the server-side room module each locally determine whether the ball has bounced off a wall or a paddle. However, goals are determined by the room module only. When the room module detects a goal, it awards a point to the client that scored, and informs both clients of the change in score by setting a game room attribute, "score":

m_ctx.getRoom().setAttribute("score", m_leftPlayerScore + "," +
  m_rightPlayerScore, Attribute.SCOPE_GLOBAL,
  Attribute.FLAG_SERVER_ONLY | Attribute.FLAG_SHARED);

In the preceding code, notice that the "score" attribute is defined as "server only," which prevents malicious clients from setting the score to an illegitimate value.

When a client receives the game's new score, it updates the screen as follows (for expository purposes, some code has been omitted from the following roomAttributeUpdateListener() method):

protected function roomAttributeUpdateListener (e:AttributeEvent):void {
  var scores:Array;
  switch (e.getChangedAttr().name) {
    case RoomAttributes.SCORE:
      scores = e.getChangedAttr().value.split(",");
      hud.setLeftPlayerScore(scores[0]);
      hud.setRightPlayerScore(scores[1]);
      break;
  }
}

After a point is awarded, the server resets the ball to the centre of the playing field, and launches it in a random direction towards a player.

private void resetBall() {
  // --- place it in the middle with initial ball speed
  m_ball.setX(COURT_WIDTH/2-BALL_SIZE/2);
  m_ball.setY(COURT_HEIGHT/2-BALL_SIZE/2);
  m_ball.setSpeed(INITIAL_BALL_SPEED);
  // --- make ball reset moving towards a player
  double dir = 0;
  if (Math.random() < .5) {
    // --- towards left player (between 135 and 225 degrees)
    dir = Math.random()*Math.PI/2+3*Math.PI/4;
  } else {
    // --- towards right player (between 315 and 45 degrees)
    dir = (Math.random()*Math.PI/2+7*Math.PI/4) % (2*Math.PI);
  }
  m_ball.setDirection(dir);
}

The room module then informs clients of the ball's new trajectory by setting a room attribute, "ball":

m_ctx.getRoom().setAttribute("ball", m_decFmt.format(m_ball.getX()) +
  "," + m_decFmt.format(m_ball.getY()) + "," + m_ball.getSpeed() +
  "," + m_decFmt.format(m_ball.getDirection()), Attribute.SCOPE_GLOBAL,
  Attribute.FLAG_SERVER_ONLY | Attribute.FLAG_SHARED);

The "ball" attribute's value includes the ball's position (x, y), speed (pixels per second), and direction (radians). For example, a typical value for the "ball" attribute might be "320,240,150,1.057". As with the "score" attribute, the "ball" attribute is defined as "server only," which prevents malicious clients from tampering with the ball's position.

The clients respond to changes in the ball's position by listening for room-attribute updates as follows (again, some code has been omitted from the following roomAttributeUpdateListener() method):

protected function roomAttributeUpdateListener (e:AttributeEvent):void {
  switch (e.getChangedAttr().name) {
    case RoomAttributes.BALL:
      deserializeBall(e.getChangedAttr().value);
      break;
  }
}

To ensure that the client-side simulations stay synchronized with the state of the server, Pong's server-side room module also sends a ball-trajectory update every five seconds. Clients correct the ball's position and trajectory whenever the "ball" attribute changes.

When two clients are already in the game room and a third client attempts to join, the third client displays a "game full" message and then attempts to re-join the room every 5 seconds.

protected function roomJoinResultListener (e:RoomEvent):void {
  // If there are already two people playing, wait 5 seconds, then
  // attempt to join the game again (hoping that someone has left)
  if (e.getStatus() == Status.ROOM_FULL) {
    hud.setStatus("Game full. Next join attempt in 5 seconds.");
    joinTimer.start();
  }
}

Finally, if the Flash client ever fails to connect or loses the connection to Union Server, it automatically attempts to reconnect every 8 seconds (as specified in the client's config.xml file). The client displays detailed information about connection status from within a low-level ConnectionManagerEvent.BEGIN_CONNECT event listener.

protected function beginConnectListener (e:ConnectionManagerEvent):void  {
  var connectAttemptCount:int =
                    reactor.getConnectionManager().getConnectAttemptCount();
  if (reactor.getConnectionManager().getReadyCount() == 0) {
    // The client has never connected before
    if (connectAttemptCount == 1) {
      hud.setStatus("Connecting to Union...");
    } else {
      hud.setStatus("Connection attempt failed. Trying again (attempt "
                    + connectAttemptCount + ")...");
    }
  } else {
    // The client has connected before, so this is a reconnection attempt
    if (connectAttemptCount == 1) {
      hud.setStatus("Disconnected from Union. Reconnecting...");
    } else {
      hud.setStatus("Reconnection attempt failed. Trying again (attempt "
                    + connectAttemptCount + ")...");
    }
  }
  gameManager.reset();
}

To see Pong's reconnection feature in action, try disconnecting your ethernet connection or turning off your computer's wireless network.

The following sections present the complete code for Union Pong. Source code is also available for download here.

Java: PongRoomModule

PongRoomModule is Pong's server-side room module, written in Java. It controls the game's flow, scoring, and physics simulation.

package net.user1.union.example.pong;

import java.text.DecimalFormat;
import net.user1.union.api.Client;
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.ClientEvent;
import net.user1.union.core.event.RoomEvent;
import net.user1.union.core.exception.AttributeException;

/**
 * This is the RoomModule that controls the pong game. The 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 room.
 */
public class PongRoomModule 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 app
    private Thread m_thread;
    // --- players and their objects
    private Client m_leftPlayer;
    private Client m_rightPlayer;
    private PongObject m_leftPaddle;
    private PongObject m_rightPaddle;
    private int m_leftPlayerScore;
    private int m_rightPlayerScore;
    // --- world objects
    private PongObject m_ball;
    // --- flag that a game is being played
    private boolean m_isGameRunning;
    // --- how often the game loop should pause (in milliseconds) between updates
    private static final int GAME_UPDATE_INTERVAL = 20;
    // --- how often the server should update clients with the world state
    // --- (i.e. the position and velocity of the ball)
    private static final long BALL_UPDATE_INTERVAL = 5000L;
    // --- game metrics
    private static final int COURT_HEIGHT = 480;            // pixels
    private static final int COURT_WIDTH = 640;             // pixels
    private static final int BALL_SIZE = 10;                // pixels
    private static final int INITIAL_BALL_SPEED = 150;      // pixels / sec
    private static final int BALL_SPEEDUP = 25;             // pixels / sec
    private static final int PADDLE_HEIGHT = 60;            // pixels
    private static final int PADDLE_WIDTH = 10;             // pixels
    private static final int PADDLE_SPEED = 300;            // pixels / sec
    private static final int WALL_HEIGHT = 10;              // pixels
    // --- attribute constants
    private static final String ATTR_PADDLE = "paddle";
    private static final String ATTR_SIDE = "side";
    private static final String ATTR_STATUS = "status";
    // --- decimal format for sending rounded values to clients
    private DecimalFormat m_decFmt = new DecimalFormat("0.##########");

    /**
     * The init method is called when the instance is created.
     */
    public boolean init(ModuleContext ctx) {
        m_ctx = ctx;

        // --- create our world objects
        m_ball = new PongObject(0,0,INITIAL_BALL_SPEED,0);

        // --- register to receive events
        m_ctx.getRoom().addEventListener(RoomEvent.ADD_CLIENT, this,
                "onAddClient");
        m_ctx.getRoom().addEventListener(RoomEvent.REMOVE_CLIENT, this,
                "onRemoveClient");

        // --- create the app thread and start it
        m_thread = new Thread(this);
        m_thread.start();

        // --- the module initialized fine
        return true;
    }

    /**
     * Called by the game thread. Contains the main game loop.
     */
    public void run() {
        long lastBallUpdate = 0;

        // --- while the room module is running
        while (m_thread != null) {
            // --- init the ticks
            long lastTick = System.currentTimeMillis();
            long thisTick;

            // --- while a game is running
            while (m_isGameRunning) {
                thisTick = System.currentTimeMillis();

                // --- update the game with the difference in ms since the
                // --- last tick
                lastBallUpdate += thisTick-lastTick;
                update(thisTick-lastTick);
                lastTick = thisTick;

                // --- check if time to send a ball update
                if (lastBallUpdate > BALL_UPDATE_INTERVAL) {
                    sendBallUpdate();
                    lastBallUpdate -= BALL_UPDATE_INTERVAL;
                }

                // --- pause game
                try {
                    Thread.sleep(GAME_UPDATE_INTERVAL);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            // --- game has stopped
            // --- wait for game to run again when enough clients join
            synchronized (this) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Update the world.
     *
     * @param tick the time in milliseconds since the last update
     */
    public void update(long tick) {
        // --- update players
        updatePaddle(m_leftPaddle, tick);
        updatePaddle(m_rightPaddle, tick);

        // --- update the ball
        updateBall(tick);
    }

    /**
     * Update the position of a paddle.
     *
     * @param paddle the paddle to update
     * @param tick the time in milliseconds since the last update
     */
    private void updatePaddle(PongObject paddle, long tick) {
        paddle.setY(Math.max(Math.min(paddle.getY()-
                Math.sin(paddle.getDirection())*paddle.getSpeed()*tick/1000,
                COURT_HEIGHT-WALL_HEIGHT-PADDLE_HEIGHT), WALL_HEIGHT));
    }

    /**
     * Update the ball
     *
     * @param tick the time in milliseconds since the last update
     */
    private void updateBall(long tick) {
        // --- determine the new X,Y without regard to game boundaries
        double ballX = m_ball.getX() +
                Math.cos(m_ball.getDirection())*m_ball.getSpeed()*tick/1000;
        double ballY = m_ball.getY() -
                Math.sin(m_ball.getDirection())*m_ball.getSpeed()*tick/1000;

        // --- set the potential new ball position which may be overridden below
        m_ball.setX(ballX);
        m_ball.setY(ballY);

        // --- determine if the ball hit a boundary
        // --- NOTE: this is a rough calculation and does not attempt to
        // --- interpolate within a tick to determine the exact position
        // --- of the ball and paddle at the potential time of a collision
        if (ballX < PADDLE_WIDTH) {
            // --- left side
            if ((ballY + BALL_SIZE > m_leftPaddle.getY()) &&
                    ballY < (m_leftPaddle.getY() + PADDLE_HEIGHT)) {
                // --- paddle hit the ball so it will appear the same distance
                // --- on the other side of the collision point and angle will
                // --- flip
                m_ball.setX(2*PADDLE_WIDTH - ballX);
                bounceBall(m_ball.getDirection() > Math.PI ? 3*Math.PI/2 : Math.PI/2);
                m_ball.setSpeed(m_ball.getSpeed() + BALL_SPEEDUP);
            } else {
                // --- increase score
                m_rightPlayerScore++;
                sendScoreUpdate();

                // --- reset ball
                resetBall();
                sendBallUpdate();
            }
        } else if (ballX > (COURT_WIDTH-PADDLE_WIDTH-BALL_SIZE)) {
            // --- right side
            if ((ballY + BALL_SIZE > m_rightPaddle.getY()) &&
                    ballY < (m_rightPaddle.getY() + PADDLE_HEIGHT)) {
                // --- paddle hit the ball so it will appear the same distance
                // --- on the other side of the collision point and angle will
                // --- flip
                m_ball.setX(2*(COURT_WIDTH-PADDLE_WIDTH-BALL_SIZE) - ballX);
                bounceBall(m_ball.getDirection() > 3*Math.PI/2 ? 3*Math.PI/2 : Math.PI/2);
                m_ball.setSpeed(m_ball.getSpeed() + BALL_SPEEDUP);
            } else {
                // --- increase score
                m_leftPlayerScore++;
                sendScoreUpdate();

                // --- reset ball
                resetBall();
                sendBallUpdate();
            }
        }

        // --- the ball may also have hit a top or bottom wall
        if (ballY < WALL_HEIGHT) {
            // --- top wall
            m_ball.setY(2*WALL_HEIGHT-ballY);
            bounceBall(m_ball.getDirection() > Math.PI/2 ? Math.PI : 2*Math.PI);
        } else if (ballY + BALL_SIZE > COURT_HEIGHT - WALL_HEIGHT) {
            // --- bottom wall
            m_ball.setY(2*(COURT_HEIGHT-WALL_HEIGHT-BALL_SIZE)-ballY);
            bounceBall(m_ball.getDirection() > 3*Math.PI/2 ? 2*Math.PI : Math.PI);
        }
    }

    /**
     * Bounces the ball off a wall. Essentially flips the angle over a given
     * axis. 0(360) degrees is to the right increasing counter-clockwise.
     * Eg. a ball moving left and bouncing off the bottom wall would be
     * "flipped" over the 180 degree axis.
     *
     * @param bounceAxis the axis to flip around
     */
    private void bounceBall(double bounceAxis) {
        m_ball.setDirection(((2*bounceAxis-m_ball.getDirection())+(2*Math.PI))%
                (2*Math.PI));
    }

    /**
     * Reset the ball.
     */
    private void resetBall() {
        // --- place it in the middle with initial ball speed
        m_ball.setX(COURT_WIDTH/2-BALL_SIZE/2);
        m_ball.setY(COURT_HEIGHT/2-BALL_SIZE/2);
        m_ball.setSpeed(INITIAL_BALL_SPEED);
        // --- make ball reset moving towards a player
        double dir = 0;
        if (Math.random() < .5) {
            // --- towards left player (between 135 and 225 degrees)
            dir = Math.random()*Math.PI/2+3*Math.PI/4;
        } else {
            // --- towards right player (between 315 and 45 degrees)
            dir = (Math.random()*Math.PI/2+7*Math.PI/4) % (2*Math.PI);
        }
        m_ball.setDirection(dir);
    }

    /**
     * Send a score update to clients.
     */
    private void sendScoreUpdate() {
        try {
            m_ctx.getRoom().setAttribute("score", m_leftPlayerScore + "," +
                    m_rightPlayerScore, Attribute.SCOPE_GLOBAL,
                    Attribute.FLAG_SERVER_ONLY | Attribute.FLAG_SHARED);
        } catch (AttributeException e) {
            e.printStackTrace();
        }
    }

    /**
     * Send a ball update to clients.
     */
    private void sendBallUpdate() {
        try {
            m_ctx.getRoom().setAttribute("ball", m_decFmt.format(m_ball.getX()) +
                    "," + m_decFmt.format(m_ball.getY()) + "," + m_ball.getSpeed() +
                    "," + m_decFmt.format(m_ball.getDirection()), Attribute.SCOPE_GLOBAL,
                    Attribute.FLAG_SERVER_ONLY | Attribute.FLAG_SHARED);
        } catch (AttributeException e) {
            e.printStackTrace();
        }
    }

    /**
     * Client joined the game.
     *
     * @param evt the RoomEvent
     */
    public void onAddClient(RoomEvent evt) {
        synchronized (this) {
            // --- listen for client attribute updates
            evt.getClient().addEventListener(ClientEvent.ATTRIBUTE_CHANGED, this,
                    "onClientAttributeChanged");

            // --- assign them a player
            if (m_leftPlayer == null) {
                m_leftPlayer = evt.getClient();
                m_leftPaddle = new PongObject(0,COURT_HEIGHT/2 - PADDLE_HEIGHT/2,
                        PADDLE_SPEED, 0);
                try {
                    m_leftPlayer.setAttribute(ATTR_SIDE, "left",
                            m_ctx.getRoom().getQualifiedID(),
                            Attribute.FLAG_SERVER_ONLY | Attribute.FLAG_SHARED);
                    m_leftPlayer.setAttribute(ATTR_STATUS, "ready",
                            m_ctx.getRoom().getQualifiedID(),
                            Attribute.FLAG_SERVER_ONLY | Attribute.FLAG_SHARED);
                } catch (AttributeException e) {
                    e.printStackTrace();
                }
            } else if (m_rightPlayer == null) {
                m_rightPlayer = evt.getClient();
                m_rightPaddle = new PongObject(COURT_WIDTH-PADDLE_WIDTH,
                        COURT_HEIGHT/2 - PADDLE_HEIGHT/2, PADDLE_SPEED, 0);
                try {
                    m_rightPlayer.setAttribute(ATTR_SIDE, "right",
                            evt.getRoom().getQualifiedID(),
                            Attribute.FLAG_SERVER_ONLY | Attribute.FLAG_SHARED);
                    m_rightPlayer.setAttribute(ATTR_STATUS, "ready",
                            m_ctx.getRoom().getQualifiedID(),
                            Attribute.FLAG_SERVER_ONLY | Attribute.FLAG_SHARED);
                } catch (AttributeException e) {
                    e.printStackTrace();
                }
            } 

            // --- is the game ready?
            if (m_leftPlayer != null && m_rightPlayer != null) {
                m_leftPlayerScore = 0;
                m_rightPlayerScore = 0;
                resetBall();
                evt.getRoom().sendMessage("START_GAME");
                sendBallUpdate();
                sendScoreUpdate();
                m_isGameRunning = true;

                // --- start the game loop going again
                notify();
            }
        }
    }

    /**
     * Client left the game.
     *
     * @param evt the RoomEvent
     */
    public void onRemoveClient(RoomEvent evt) {
        synchronized (this) {
            // --- stop listening for attribute changes for this client
            evt.getClient().removeEventListener(ClientEvent.ATTRIBUTE_CHANGED,
                    this, "onClientAttributeChanged");

            // --- remove them as a player
            if (evt.getClient().equals(m_leftPlayer)) {
                m_leftPlayer = null;
            } else if (evt.getClient().equals(m_rightPlayer)) {
                m_rightPlayer = null;
            }

            // --- game must be stopped
            evt.getRoom().sendMessage("STOP_GAME");
            m_leftPlayerScore = 0;
            m_rightPlayerScore = 0;
            sendScoreUpdate();
            m_isGameRunning = false;
        }
    }

    /**
     * A client attribute changed.
     *
     * @param evt the ClientEvent
     */
    public void onClientAttributeChanged(ClientEvent evt) {
        // --- was the attribute scoped to this room and for the paddle?
        if (evt.getAttribute().getScope().equals(m_ctx.getRoom().getQualifiedID())
                && evt.getAttribute().getName().equals(ATTR_PADDLE)) {
            // --- then update the paddle object
            PongObject paddle = null;
            String[] paddleAttrs = evt.getAttribute().nullSafeGetValue().split(",");
            if (evt.getClient().equals(m_leftPlayer)) {
                paddle = m_leftPaddle;
            } else if (evt.getClient().equals(m_rightPlayer)) {
                paddle = m_rightPaddle;
            }

            // --- parse the attribute and set the paddle
            if (paddle != null) {
                paddle.setX(Float.parseFloat(paddleAttrs[0]));
                paddle.setY(Float.parseFloat(paddleAttrs[1]));
                paddle.setSpeed(Integer.parseInt(paddleAttrs[2]));
                paddle.setDirection(Float.parseFloat(paddleAttrs[3]));
            }
        }
    }

    /**
     * The shutdown method is called when the server removes the room which
     * also removes the room module.
     */
    public void shutdown() {
        // --- deregister for events
        m_ctx.getRoom().removeEventListener(RoomEvent.ADD_CLIENT, this, "onAddClient");
        m_ctx.getRoom().removeEventListener(RoomEvent.REMOVE_CLIENT, this, "onRemoveClient");

        m_thread = null;
    }

    /**
     * A Object within the Pong game (ball and paddle).
     */
    public class PongObject {
        private double m_x; // x pixel position
        private double m_y; // y pixel position
        private int m_speed; // pixels per second
        private double m_direction; // direction the ball is traveling in radians

        public PongObject(float x, float y, int speed, float direction) {
            m_x = x;
            m_y = y;
            m_speed = speed;
            m_direction = direction;
        }

        public void setX(double x) {
            m_x = x;
        }

        public double getX() {
            return m_x;
        }

        public void setY(double y) {
            m_y = y;
        }

        public double getY() {
            return m_y;
        }

        public double getDirection() {
            return m_direction;
        }

        public void setDirection(double direction) {
            m_direction = direction;
        }

        public void setSpeed(int speed) {
            m_speed = speed;
        }

        public int getSpeed() {
            return m_speed;
        }
    }
}

ActionScript: UnionPong

UnionPong is the Flash client's main application class. It connects to the server and boots the game.

package {
  import flash.display.Sprite;

  import net.user1.reactor.ConnectionManagerEvent;
  import net.user1.reactor.Reactor;
  import net.user1.reactor.ReactorEvent;
  import net.user1.reactor.Room;
  import net.user1.reactor.ModuleType;
  import net.user1.reactor.RoomModules;
  import net.user1.reactor.RoomSettings;

  /**
   * The pong application's main class.
   */
  public class UnionPong extends Sprite {
// =============================================================================
// VARIABLES
// =============================================================================
    // The core Reactor object that connects to Union Server
    protected var reactor:Reactor;
    // Accepts keyboard input
    protected var keyboardController:KeyboardController;
    // Controls game flow logic and physics simulation
    protected var gameManager:GameManager;
    // The playing field graphics
    protected var court:Court;
    // Heads-up display, including scores and status messages
    protected var hud:HUD;

// =============================================================================
// CONSTRUCTOR
// =============================================================================
    public function UnionPong () {
      // Make the keyboard controller
      keyboardController = new KeyboardController(stage);

      // Make the Reactor object and connect to Union Server
      reactor = new Reactor("config.xml");
      reactor.addEventListener(ReactorEvent.READY, readyListener);
      reactor.getConnectionManager().addEventListener(
        ConnectionManagerEvent.BEGIN_CONNECT,
        beginConnectListener);

      // Tell Reactor to use PongClient as the class for clients in this app
      reactor.getClientManager().setDefaultClientClass(PongClient);

      // Make the heads-up display
      hud = new HUD();
      addChild(hud);

      // Make the playing field graphic
      court = new Court();
      addChild(court);

      // Make the game manager
      gameManager = new GameManager(court, hud);
    }

// =============================================================================
// CONNECTION MANAGER EVENT LISTENERS
// =============================================================================
    // Triggered when the client begins a connection attempt
    protected function beginConnectListener (e:ConnectionManagerEvent):void  {
      var connectAttemptCount:int =
                        reactor.getConnectionManager().getConnectAttemptCount();
      if (reactor.getConnectionManager().getReadyCount() == 0) {
        // The client has never connected before
        if (connectAttemptCount == 1) {
          hud.setStatus("Connecting to Union...");
        } else {
          hud.setStatus("Connection attempt failed. Trying again (attempt "
                        + connectAttemptCount + ")...");
        }
      } else {
        // The client has connected before, so this is a reconnection attempt
        if (connectAttemptCount == 1) {
          hud.setStatus("Disconnected from Union. Reconnecting...");
        } else {
          hud.setStatus("Reconnection attempt failed. Trying again (attempt "
                        + connectAttemptCount + ")...");
        }
      }
      gameManager.reset();
    }

// =============================================================================
// REACTOR EVENT LISTENERS
// =============================================================================
    // Triggered when the connection is established and ready for use
    protected function readyListener (e:ReactorEvent):void  {
      initGame();
    }

// =============================================================================
// GAME SETUP
// =============================================================================
    // Performs game setup
    protected function initGame ():void {
      // Give the keyboard controller a reference to the current
      // client (reactor.self()), which it uses to send user input to the server
      keyboardController.setClient(PongClient(reactor.self()));
      // Specify the server side room module for the pong room
      var modules:RoomModules = new RoomModules();
      modules.addModule("net.user1.union.example.pong.PongRoomModule",
                        ModuleType.CLASS);
      // Set the room-occupant limit to two, and make the game room permanent
      var settings:RoomSettings = new RoomSettings();
      settings.maxClients = 2;
      settings.removeOnEmpty = false;
      // Create the game room
      var room:Room = reactor.getRoomManager().createRoom(Settings.GAME_ROOMID,
                                                          settings,
                                                          null,
                                                          modules);
      // Give the game manager a reference to the game room, which supplies
      // game-state updates from the server
      gameManager.setRoom(room);
    }
  }
}

ActionScript: GameManager

GameManager manages client-side game flow logic and physics simulation.

package {
  import flash.events.TimerEvent;
  import flash.utils.Timer;
  import flash.utils.getTimer;

  import net.user1.reactor.AttributeEvent;
  import net.user1.reactor.IClient;
  import net.user1.reactor.Room;
  import net.user1.reactor.RoomEvent;
  import net.user1.reactor.Status;

  /**
   * Manages game flow logic and physics simulation.
   */
  public class GameManager {
// =============================================================================
// VARIABLES
// =============================================================================
    // Playing-field simulation objects
    protected var leftPlayer:PongClient;
    protected var rightPlayer:PongClient;
    protected var ball:PongObject;

    // Dependencies
    protected var room:Room;
    protected var court:Court;
    protected var hud:HUD;

    // Game tick
    protected var lastUpdate:int;
    protected var updateTimer:Timer;

    // Current game state
    protected var state:int;

    // Join-game timer (used when the game is full)
    protected var joinTimer:Timer;

// =============================================================================
// CONSTRUCTOR
// =============================================================================
    public function GameManager (court:Court, hud:HUD) {
      this.court = court;
      this.hud = hud;

      // Make the simulated ball
      ball = new PongObject();
      ball.width = Settings.BALL_SIZE;

      // Make the game-tick timer
      updateTimer = new Timer(Settings.GAME_UPDATE_INTERVAL);
      updateTimer.addEventListener(TimerEvent.TIMER, timerListener);
      updateTimer.start();

      // Make the join-game timer
      joinTimer = new Timer(5000, 1);
      joinTimer.addEventListener(TimerEvent.TIMER, joinTimerListener);
    }

// =============================================================================
// DEPENDENCIES
// =============================================================================

    // Supplies this game manager with a Room object representing the
    // server-side pong game room
    public function setRoom (room:Room):void {
      // Remove event listeners and message listeners from the old
      // Room object (if there is one)
      if (this.room != null) {
        removeRoomListeners();
      }
      // Store the room reference
      this.room = room;
      // Add event listeners and message listeners to the supplied Room object
      if (room != null) {
        addRoomListeners();
      }
      // Join the game
      room.join();
      // Display status on screen
      hud.setStatus("Joining game...");
    }

    public function addRoomListeners ():void {
      room.addEventListener(RoomEvent.JOIN, roomJoinListener);
      room.addEventListener(RoomEvent.JOIN_RESULT, roomJoinResultListener);
      room.addEventListener(AttributeEvent.UPDATE, roomAttributeUpdateListener);
      room.addEventListener(RoomEvent.UPDATE_CLIENT_ATTRIBUTE, clientAttributeUpdateListener);
      room.addEventListener(RoomEvent.REMOVE_OCCUPANT, removeOccupantListener);

      room.addMessageListener(RoomMessages.START_GAME, startGameListener);
      room.addMessageListener(RoomMessages.STOP_GAME, stopGameListener);
    }

    public function removeRoomListeners ():void {
      room.removeEventListener(RoomEvent.JOIN, roomJoinListener);
      room.removeEventListener(RoomEvent.JOIN_RESULT, roomJoinResultListener);
      room.removeEventListener(AttributeEvent.UPDATE, roomAttributeUpdateListener);
      room.removeEventListener(RoomEvent.UPDATE_CLIENT_ATTRIBUTE, clientAttributeUpdateListener);
      room.removeEventListener(RoomEvent.REMOVE_OCCUPANT, removeOccupantListener);

      room.removeMessageListener(RoomMessages.START_GAME, startGameListener);
      room.removeMessageListener(RoomMessages.STOP_GAME, stopGameListener);
    }

// =============================================================================
// ROOM EVENT LISTENERS
// =============================================================================

    // Triggered when the current client successfully joins the game room
    protected function roomJoinListener (e:RoomEvent):void {
      state = GameStates.WAITING_FOR_OPPONENT;
      hud.setStatus("Waiting for opponent...");
      initPlayers();
    }

    // Triggered when the server reports the result of an attempt
    // to join the game room
    protected function roomJoinResultListener (e:RoomEvent):void {
      // If there are already two people playing, wait 5 seconds, then
      // attempt to join the game again (hoping that someone has left)
      if (e.getStatus() == Status.ROOM_FULL) {
        hud.setStatus("Game full. Next join attempt in 5 seconds.");
        joinTimer.start();
      }
    }

    // Triggered when one of the room's attributes changes. This method
    // handles ball and score updates sent by the server.
    protected function roomAttributeUpdateListener (e:AttributeEvent):void {
      var scores:Array;

      switch (e.getChangedAttr().name) {
        // When the "ball" attribute changes, synchronize the ball
        case RoomAttributes.BALL:
          deserializeBall(e.getChangedAttr().value);
          break;

        // When the "score" attribute changes, update player scores
        case RoomAttributes.SCORE:
          scores = e.getChangedAttr().value.split(",");
          hud.setLeftPlayerScore(scores[0]);
          hud.setRightPlayerScore(scores[1]);
          break;
      }
    }

    // Triggered when a room occupant's attribute changes. This method
    // handles changes in player status.
    protected function clientAttributeUpdateListener (e:RoomEvent):void {
      // If the client is now ready (i.e., the "status" attribute's value is
      // now "ready"), add the client to the game simulation
      if (e.getChangedAttr().name == ClientAttributes.STATUS
          && e.getChangedAttr().scope == Settings.GAME_ROOMID
          && e.getChangedAttr().value == PongClient.STATUS_READY) {
        addPlayer(PongClient(e.getClient()));
      }
    }

    // Triggered when a client leaves the room. This method responds to players
    // leaving the game.
    protected function removeOccupantListener (e:RoomEvent):void {
      state = GameStates.WAITING_FOR_OPPONENT;
      hud.setStatus("Opponent left the game");
      removePlayer(PongClient(e.getClient()));
    }

// =============================================================================
// ROOM MESSAGE LISTENERS
// =============================================================================

    // Triggered when the server sends a START_GAME message
    protected function startGameListener (fromClient:IClient):void {
      lastUpdate = getTimer();
      hud.setStatus("");
      resetBall();
      court.showBall();
      hud.resetScores();
      state = GameStates.IN_GAME;
    }

    // Triggered when the server sends a STOP_GAME message
    protected function stopGameListener (fromClient:IClient):void {
      court.hideBall();
    }

// =============================================================================
// JOIN-GAME TIMER LISTENER
// =============================================================================

    // Triggered every five seconds when the game room is full
    public function joinTimerListener (e:TimerEvent):void {
      hud.setStatus("Joining game...");
      room.join();
    }

// =============================================================================
// GAME TICK
// =============================================================================

    // Triggered every 20 milliseconds. Updates the playing-field simulation.
    public function timerListener (e:TimerEvent):void {
      var now:int = getTimer();
      var elapsed:int = now - lastUpdate;
      lastUpdate = now;

      // If the game is not in progress, update the players only
      var s:int = getTimer();
      switch (state) {
        case GameStates.WAITING_FOR_OPPONENT:
          updatePlayer(leftPlayer, elapsed);
          updatePlayer(rightPlayer, elapsed);
          break;

        // If the game is in progress, update the players and the ball
        case GameStates.IN_GAME:
          updatePlayer(leftPlayer, elapsed);
          updatePlayer(rightPlayer, elapsed);
          updateBall(elapsed);
          break;
      }
    }

// =============================================================================
// PLAYER MANAGEMENT
// =============================================================================

    // Adds all "ready" players to the game simulation. Invoked when the
    // current client joins the game room.
    public function initPlayers ():void {
      for each (var player:PongClient in room.getOccupants()) {
        if (player.getAttribute(ClientAttributes.STATUS, Settings.GAME_ROOMID)
            == PongClient.STATUS_READY) {
          addPlayer(player);
        }
      }
    }

    // Adds a new "ready" player to the game simulation. Invoked when a foreign
    // client becomes ready after the current client is already in the game room.
    protected function addPlayer (player:PongClient):void {
      if (player.getSide() == PongClient.SIDE_LEFT) {
        leftPlayer = player;
        court.setLeftPaddlePosition(player.getPaddle().x, player.getPaddle().y);
        court.showLeftPaddle();
      } else if (player.getSide() == PongClient.SIDE_RIGHT) {
        rightPlayer = player;
        court.setRightPaddlePosition(player.getPaddle().x, player.getPaddle().y);
        court.showRightPaddle();
      }
    }

    // Removes a player from the game simulation. Invoked whenever a client
    // leaves the game room.
    protected function removePlayer (player:PongClient):void {
      if (player.getSide() == PongClient.SIDE_LEFT) {
        leftPlayer = null;
        court.hideLeftPaddle();
      } else if (player.getSide() == PongClient.SIDE_RIGHT) {
        rightPlayer = null;
        court.hideRightPaddle();
      }
    }

// =============================================================================
// WORLD SIMULATION/PHYSICS
// =============================================================================

    // Places the ball in the middle of the court
    protected function resetBall ():void {
      ball.x = Settings.COURT_WIDTH/2 - Settings.BALL_SIZE/2;
      ball.y = Settings.COURT_HEIGHT/2 - Settings.BALL_SIZE/2;
      ball.speed = 0;
      court.setBallPosition(ball.x, ball.y);
    }

    // Updates the specified player's paddle position based on its most recent
    // known speed and direction
    protected function updatePlayer (player:PongClient, elapsed:int):void {
      var newPaddleY:Number;
      if (player != null) {
        // Calculate new paddle position
        newPaddleY = player.getPaddle().y
                   + Math.sin(-player.getPaddle().direction)
                   * player.getPaddle().speed * elapsed/1000;

        player.getPaddle().y = clamp(newPaddleY,
          0 + Settings.WALL_HEIGHT,
          Settings.COURT_HEIGHT - Settings.PADDLE_HEIGHT - Settings.WALL_HEIGHT);

        // Reposition appropriate paddle graphic
        if (player.getSide() == PongClient.SIDE_LEFT) {
          court.setLeftPaddlePosition(0, player.getPaddle().y);
        } else if (player.getSide() == PongClient.SIDE_RIGHT) {
          court.setRightPaddlePosition(Settings.COURT_WIDTH - Settings.PADDLE_WIDTH,
                                       player.getPaddle().y);
        }
      }
    }

    // Updates the ball's paddle position based on its most recent
    // known speed and direction
    protected function updateBall (elapsed:int):void {
      // Calculate the position the ball would be in if there were
      // no walls and no paddles
      var ballX:Number = ball.x + Math.cos(ball.direction)*ball.speed*elapsed/1000;
      var ballY:Number = ball.y - Math.sin(ball.direction)*ball.speed*elapsed/1000;
      ball.x = ballX;
      ball.y = ballY;

      // Adjust the ball's position if it hits a paddle this tick
      if (ballX < Settings.PADDLE_WIDTH) {
        if (ballY + Settings.BALL_SIZE > leftPlayer.getPaddle().y
            && ballY < (leftPlayer.getPaddle().y + Settings.PADDLE_HEIGHT)) {
          ball.x = 2*Settings.PADDLE_WIDTH - ballX;
          bounceBall(ball.direction >  Math.PI ? 3*Math.PI/2 :  Math.PI/2);
          ball.speed += Settings.BALL_SPEEDUP;
        }
      } else if (ballX > (Settings.COURT_WIDTH-Settings.PADDLE_WIDTH-Settings.BALL_SIZE)) {
        if (ballY + Settings.BALL_SIZE > rightPlayer.getPaddle().y
            && ballY < (rightPlayer.getPaddle().y + Settings.PADDLE_HEIGHT)) {
          ball.x = 2*(Settings.COURT_WIDTH - Settings.PADDLE_WIDTH - Settings.BALL_SIZE) - ballX;
          bounceBall(ball.direction >  3*Math.PI/2 ?  3*Math.PI/2 :  Math.PI/2);
          ball.speed += Settings.BALL_SPEEDUP;
        }
      }

      // Adjust the ball's position if it hits a wall this tick
      if (ballY < Settings.WALL_HEIGHT) {
        ball.y = 2*Settings.WALL_HEIGHT-ballY;
        bounceBall(ball.direction > Math.PI/2 ? Math.PI : 2*Math.PI);
      } else if (ballY + Settings.BALL_SIZE > Settings.COURT_HEIGHT - Settings.WALL_HEIGHT) {
        ball.y = 2*(Settings.COURT_HEIGHT-Settings.WALL_HEIGHT-Settings.BALL_SIZE)-ballY;
        bounceBall(ball.direction > 3*Math.PI/2 ? 2*Math.PI : Math.PI);
      }

      // Reposition the ball graphic
      court.setBallPosition(ball.x, ball.y);
    }

    // Helper function to perform "bounce" calculations
    private function bounceBall (bounceAxis:Number):void {
      ball.direction = ((2*bounceAxis-ball.direction)+(2*Math.PI))%(2*Math.PI);
    }

// =============================================================================
// SYSTEM RESET
// =============================================================================

    // Returns the entire game manager to its default state. This method is
    // invoked each time the current client attempts to connect to the server.
    public function reset ():void {
      state = GameStates.INITIALIZING;
      joinTimer.stop();
      hud.resetScores();
      court.hideBall();
      court.hideRightPaddle();
      court.hideLeftPaddle();
      leftPlayer = null;
      rightPlayer = null;
    }

// =============================================================================
// DATA DESERIALIZATION
// =============================================================================

    // Converts a serialized string representation of the ball to actual ball
    // object variable values. Invoked when the current client receives a
    // ball update from the server.
    public function deserializeBall (value:String):void {
      var values:Array = value.split(",");
      ball.x         = parseInt(values[0]);
      ball.y         = parseInt(values[1]),
      ball.speed     = parseInt(values[2]),
      ball.direction = parseFloat(values[3]);
    }
  }
}

ActionScript: PongClient

PongClient is a custom client class representing a client (player) in the Pong game. The UnionPong class specifies that all clients should be PongClient instances via ClientManager's setDefaultClientClass() method:

reactor.getClientManager().setDefaultClientClass(PongClient);

Here is the PongClient class:

package {
  import net.user1.reactor.AttributeEvent;
  import net.user1.reactor.CustomClient;

  /**
   * Represents a client (player) in the pong room.
   */
  public class PongClient extends CustomClient {
// =============================================================================
// VARIABLES
// =============================================================================
    // Player-related constants
    public static const STATUS_READY:String = "ready";
    public static const SIDE_RIGHT:String = "right";
    public static const SIDE_LEFT:String = "left";
    // The player's simulated paddle
    protected var paddle:PongObject;

// =============================================================================
// CONSTRUCTOR
// =============================================================================
    public function PongClient () {
      paddle = new PongObject();
      paddle.height = Settings.PADDLE_HEIGHT;
      paddle.width  = Settings.PADDLE_WIDTH;
    }

// =============================================================================
// INITIALIZATION
// =============================================================================
    override public function init ():void {
      addEventListener(AttributeEvent.UPDATE, updateAttributeListener);
      var paddleData:String = getAttribute(ClientAttributes.PADDLE, Settings.GAME_ROOMID);
      if (paddleData != null) {
        deserializePaddle(paddleData);
      } else {
        paddle.y = Settings.COURT_HEIGHT/2 - Settings.PADDLE_HEIGHT/2;
      }
    }

// =============================================================================
// DATA ACCESS
// =============================================================================
    public function getPaddle ():PongObject {
      return paddle;
    }

    public function getSide ():String {
      return getAttribute(ClientAttributes.SIDE, Settings.GAME_ROOMID);
    }

// =============================================================================
// CLIENT-TO-SERVER COMMUNICATION
// =============================================================================
    // Sends the current client's paddle information to the server by setting
    // a client attribute name "paddle"
    public function commit ():void {
      setAttribute(ClientAttributes.PADDLE,
                   paddle.x + "," + paddle.y + "," + paddle.speed + "," + paddle.direction,
                   Settings.GAME_ROOMID);
    }

// =============================================================================
// SERVER-TO-CLIENT COMMUNICATION
// =============================================================================
    // Triggered when one of this client's attributes changes
    public function updateAttributeListener (e:AttributeEvent):void {
      // If the "paddle" attribute changes, update this client's paddle
      if (e.getChangedAttr().name == ClientAttributes.PADDLE
          && e.getChangedAttr().scope == Settings.GAME_ROOMID
          && e.getChangedAttr().byClient == null) {
        deserializePaddle(e.getChangedAttr().value);
      }
    }

// =============================================================================
// DATA DESERIALIZATION
// =============================================================================
    // Converts a serialized string representation of the paddle to actual
    // paddle object variable values. Invoked when this client receives a
    // paddle update from the server.
    protected function deserializePaddle (value:String):void {
      var values:Array = value.split(",");
      paddle.x = parseInt(values[0]);
      paddle.y = parseInt(values[1]);
      paddle.speed = parseInt(values[2]);
      paddle.direction = parseFloat(values[3]);
    }
  }
}

ActionScript: KeyboardController

KeyboardController receives keyboard input from the user and sends it to Union Server.

package {
  import flash.display.Stage;
  import flash.events.KeyboardEvent;
  import flash.ui.Keyboard;

  /**
   * Receives keyboard input from the user and sends it to Union Server
   */
  public class KeyboardController {
// =============================================================================
// VARIABLES
// =============================================================================
    // A reference to the current client, used to send paddle
    // updates to Union Server
    protected var client:PongClient;

// =============================================================================
// CONSTRUCTOR
// =============================================================================
    public function KeyboardController (stage:Stage) {
      stage.addEventListener(KeyboardEvent.KEY_UP, keyUpListener);
      stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDownListener);
    }

// =============================================================================
// DEPENDENCIES
// =============================================================================
    public function setClient (client:PongClient):void {
      this.client = client;
    }

// =============================================================================
// KEYBOARD LISTENERS
// =============================================================================
    protected function keyDownListener (e:KeyboardEvent):void {
      if (client != null) {
        if (e.keyCode == Keyboard.UP) {
          if (client.getPaddle().direction != Settings.UP
              || client.getPaddle().speed != Settings.PADDLE_SPEED) {
            client.getPaddle().speed = Settings.PADDLE_SPEED;
            client.getPaddle().direction = Settings.UP;
            // Send the new paddle direction (up) to the server
            client.commit();
          }
        } else if (e.keyCode == Keyboard.DOWN) {
          if (client.getPaddle().direction != Settings.DOWN
              || client.getPaddle().speed != Settings.PADDLE_SPEED) {
            client.getPaddle().speed = Settings.PADDLE_SPEED;
            client.getPaddle().direction = Settings.DOWN;
            // Send the new paddle direction (down) to the server
            client.commit();
          }
        }
      }
    }

    protected function keyUpListener (e:KeyboardEvent):void {
      if (client != null) {
        if (e.keyCode == Keyboard.UP || e.keyCode == Keyboard.DOWN) {
          if (client.getPaddle().speed != 0) {
            client.getPaddle().speed = 0;
            // Send the new paddle speed (stopped) to the server
            client.commit();
          }
        }
      }
    }
  }
}

ActionScript: PongObject

PongObject represents a simple movable 2D object in the Pong physics simulation. The ball and player paddles in the simulation are all instances of PongObject.

package {

  /**
   * Represents a simple movable 2D object in the pong physics simulation.
   */
  public class PongObject {
    public var x:Number = 0;
    public var y:Number = 0;
    public var direction:Number = 0;
    public var speed:int;
    public var width:int;
    public var height:int;
  }
}

 

ActionScript: Court

Court is the visual container for the graphics in the Pong playing field, including the walls, the ball, and the player paddles.

package {
  import flash.display.Sprite;

  /**
   * A container for the graphics in the pong game.
   */
  public class Court extends Sprite {
// =============================================================================
// VARIABLES
// =============================================================================
    protected var leftPaddleGraphic:Rectangle;
    protected var rightPaddleGraphic:Rectangle;
    protected var topWallGraphic:Rectangle;
    protected var bottomWallGraphic:Rectangle;
    protected var ballGraphic:Rectangle;

// =============================================================================
// CONSTRUCTOR
// =============================================================================
    public function Court () {
      // Create graphics
      leftPaddleGraphic = new Rectangle(Settings.PADDLE_WIDTH, Settings.PADDLE_HEIGHT, 0xFFFFFF);
      rightPaddleGraphic = new Rectangle(Settings.PADDLE_WIDTH, Settings.PADDLE_HEIGHT, 0xFFFFFF);
      ballGraphic = new Rectangle(Settings.BALL_SIZE, Settings.BALL_SIZE, 0xFFFFFF);
      topWallGraphic = new Rectangle(Settings.COURT_WIDTH, Settings.WALL_HEIGHT, 0xFFFFFF);
      bottomWallGraphic = new Rectangle(Settings.COURT_WIDTH, Settings.WALL_HEIGHT, 0xFFFFFF);
      bottomWallGraphic.y = Settings.COURT_HEIGHT - Settings.WALL_HEIGHT;

      // Add wall graphics to the stage
      addChild(topWallGraphic);
      addChild(bottomWallGraphic);
    }

    public function setBallPosition (x:int, y:int):void {
      ballGraphic.x = x;
      ballGraphic.y = y;
    }

    public function setLeftPaddlePosition (x:int, y:int):void {
      leftPaddleGraphic.x = x;
      leftPaddleGraphic.y = y;
    }

    public function setRightPaddlePosition (x:int, y:int):void {
      rightPaddleGraphic.x = x;
      rightPaddleGraphic.y = y;
    }

    public function showBall ():void {
      addChild(ballGraphic);
    }

    public function hideBall ():void {
      if (contains(ballGraphic)) {
        removeChild(ballGraphic);
      }
    }

    public function showLeftPaddle ():void {
      addChild(leftPaddleGraphic);
    }

    public function hideLeftPaddle ():void {
      if (contains(leftPaddleGraphic)) {
        removeChild(leftPaddleGraphic);
      }
    }

    public function showRightPaddle ():void {
      addChild(rightPaddleGraphic);
    }

    public function hideRightPaddle ():void {
      if (contains(rightPaddleGraphic)) {
        removeChild(rightPaddleGraphic);
      }
    }
  }
}

ActionScript: HUD

HUD is a container for the "heads up display" of the pong game. It contains text fields for the players' scores and a status message. The HUD container is layered on top of the game's Court instance.

package {
  import flash.display.Sprite;
  import flash.text.TextField;
  import flash.text.TextFormat;
  import flash.text.TextFormatAlign;

  /**
   * A container for the "heads up display" of the pong game. Contains the
   * players' scores and a status message text field.
   */
  public class HUD extends Sprite {
// =============================================================================
// VARIABLES
// =============================================================================
    protected var leftPlayerScore:TextField;
    protected var rightPlayerScore:TextField;
    protected var status:TextField;    

// =============================================================================
// CONSTRUCTOR
// =============================================================================
    public function HUD () {
      var format:TextFormat = new TextFormat("_typewriter", 32, 0xFFFFFF, true);
      leftPlayerScore = new TextField();
      leftPlayerScore.selectable = false;
      leftPlayerScore.defaultTextFormat = format;
      leftPlayerScore.x = 50;
      leftPlayerScore.y = 10;
      setLeftPlayerScore(0);
      addChild(leftPlayerScore);

      rightPlayerScore = new TextField();
      rightPlayerScore.selectable = false;
      rightPlayerScore.defaultTextFormat = format;
      rightPlayerScore.x = Settings.COURT_WIDTH - 80;
      rightPlayerScore.y = 10;
      setRightPlayerScore(0);
      addChild(rightPlayerScore);

      format = new TextFormat("_typewriter", 16, 0xFFFFFF, true);
      format.align = TextFormatAlign.CENTER;
      status = new TextField();
      status.selectable = false;
      status.defaultTextFormat = format;
      status.width = Settings.COURT_WIDTH;
      status.height = 30;
      status.y = Settings.COURT_HEIGHT - status.height - 10;
      addChild(status);
    }

    public function setRightPlayerScore (score:int):void {
      rightPlayerScore.text = String(score);
    }

    public function setLeftPlayerScore (score:int):void {
      leftPlayerScore.text = String(score);
    }

    public function resetScores ():void {
      setRightPlayerScore(0);
      setLeftPlayerScore(0);
    }

    public function setStatus (msg:String):void {
      status.text = msg;
    }
  }
}

ActionScript: Rectangle

Rectangle is an on-screen rectangle graphic. The game's ball graphic, paddle graphics, and wall graphics are all instances of Rectangle.

package {
  import flash.display.Sprite;

  /**
   * An on-screen rectangle graphic.
   */
  public class Rectangle extends Sprite {
    public function Rectangle (width:int, height:int, color:uint) {
      graphics.beginFill(color);
      graphics.drawRect(0, 0, width, height);
    }
  }
}

 

ActionScript: RoomAttributes

RoomAttributes defines constants for the game-room attributes used in the Pong application. The game room attributes are the game environment's multiuser variables; their values are automatically shared with all connected clients.

package {
  /**
   * An enumeration of pong room attribute names.
   */
  public final class RoomAttributes {
    public static const SCORE:String   = "score";
    public static const BALL:String    = "ball";
  }
}

 

ActionScript: ClientAttributes

ClientAttributes defines constants for the client (player) attributes used in the Pong application. The client attributes are the players' multiuser variables; their values are automatically shared with the server and all connected clients.

package {
  /**
   * An enumeration of pong client attribute names.
   */
  public final class ClientAttributes {
    public static const SIDE:String   = "side";
    public static const PADDLE:String = "paddle";
    public static const STATUS:String = "status";
  }
}

 

ActionScript: RoomMessages

RoomMessages defines constants for the room messages used in the Pong application. In Pong, all room messages are sent by the server-side room module to players in the game room.

package {
  /**
   * An enumeration of server-to-client room message names.
   */
  public final class RoomMessages {
    public static const START_GAME:String = "START_GAME";
    public static const STOP_GAME:String = "STOP_GAME";
  }
}

 

ActionScript: Settings

Settings defines constants for the global settings of the Pong application.

package {
  /**
   * An enumeration of application settings.
   */
  public final class Settings {
    // Game room
    public static const GAME_ROOMID:String = "examples.pong";

    // Game settings
    public static const GAME_UPDATE_INTERVAL:int = 20;
    public static const PADDLE_WIDTH:int = 10;
    public static const PADDLE_HEIGHT:int = 60;
    public static const PADDLE_SPEED:int = 300;
    public static const BALL_SIZE:int = 10;
    public static const BALL_SPEEDUP:int = 25;
    public static const WALL_HEIGHT:int = 10;
    public static const COURT_WIDTH:int = 640;
    public static const COURT_HEIGHT:int = 480;
    public static const UP:Number = Math.floor((1000*(Math.PI/2)))/1000;
    public static const DOWN:Number = Math.floor((1000*((3*Math.PI)/2)))/1000;
  }
}

 

ActionScript: GameStates

GameStates defines constants for the possible states of a Pong client. The game's current state is set by the GameManager.

package {
  /**
   * An enumeration of game states.
   */
  public final class GameStates {
    public static const INITIALIZING:int = 0;
    public static const WAITING_FOR_OPPONENT:int = 2;
    public static const IN_GAME:int = 1;
  }
}

 

ActionScript: clamp()

The clamp() function limits a number to a valid range, such as 0-480. It is used to prevent the player paddles from leaving the playing field.

package {
  /**
   * Forces a value into a certain range. For example, given
   * the range 7-10, the value 5 would return 7, the value 8 would return 8,
   * and the value 145 would return 10.
   */
  public function clamp (value:Number, min:Number, max:Number):Number {
    value = Math.max(value, min);
    value = Math.min(value, max);
    return value;
  }
}

 

config.xml

Here is the XML-based configuration file loaded by the ActionScript UnionPong class.

<?xml version="1.0"?>
<config>
  <connections>
    <connection host="tryunion.com" port="80"/>
  </connections>

  <!-- Milliseconds between automatic reconnection attempts. -->
  <autoreconnectfrequency>8000</autoreconnectfrequency>
  <!-- Force a disconnection if server responses take longer than 6 seconds.-->
  <connectiontimeout>6000</connectiontimeout>
  <!-- Check the client's ping time every 4 seconds. -->
  <heartbeatfrequency>4000</heartbeatfrequency>

  <logLevel>INFO</logLevel>
</config>

Pages: 1 2