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:
- In Union Server's root directory, copy /examples/union_examples.jar to /modules/union_examples.jar.
- Compile a .swf file from the ActionScript source included in the Union Pong source archive.
- Copy the file /config/config.xml into the the same directory as the .swf file from Step 2.
- Change the host and port listed in config.xml to your server's host and port.
- 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