SimpleAVRG/Part 3

This is part three of the SimpleAVRG tutorial. It details the Room Property Space.

While working on an AVRG, you may want to keep browser tabs open to these pages:
 * http://www.whirled.com/code/asdocs/
 * http://www.whirled.com/code/asdocs/com/whirled/avrg/AVRGameControl.html
 * http://www.whirled.com/code/asdocs/com/whirled/avrg/AVRServerGameControl.html
 * http://livedocs.adobe.com/flex/3/langref/index.html

Adding a Room Monster Counter
Now we'll update the system to add monsters to particular rooms instead of just to the game as a whole. They still won't be visible or interactive--we'll get to that later--but they'll be reported to rooms containing players and they'll be removed from rooms that all of the players have left.

This step will add significantly more logging, so we're switching from simple trace statements to the more versatile Log class.

We'll add a Dictionary to store the monster counts per room and rename the maybeAddMonster method to maybeAddMonsters to reflect its new behavior.

Server.as
...

public function Server {       _log.info("SimpleAVRG server agent reporting for duty!");

...

// start with no monsters _numMonsters = 0; _control.game.props.set(SimpleAVRGConstants.GLOBAL_MONSTER_COUNT, 0); _roomMonsters = new Dictionary;

// add monsters periodically _monsterTimer = new Timer(ADD_MONSTER_DELAY); _monsterTimer.addEventListener(TimerEvent.TIMER, maybeAddMonsters); _monsterTimer.start; }

...

// This is the number of monsters in each room. protected var _roomMonsters :Dictionary;

// The logger. protected var _log :Log = Log.getLog(Server);

...

The maybeAddMonsters method has several changes in addition to its new name. It now considers adding a monster for each player in the game and sends the monsters to specific rooms.

Server.as ...

// Consider adding monsters. protected function maybeAddMonsters (event :TimerEvent) :void {       // For each player in the game, consider adding a monster to that // player's location. This means that rooms with more players in them // have greater odds of receiving monsters. for each (var playerId :int in _control.game.getPlayerIds) { if (Math.random <= ADD_MONSTER_CHANCE) { addMonster( _control.getPlayer(playerId).getRoomId ); }       }    }

...

The addMonster method also receives an overhaul. It now takes a room id as an argument and updates the room's monster count in the _roomMonsters Dictionary before setting the room's ROOM_MONSTER_COUNT property to the new value.

Server.as ...

// Add a monster to a room and broadcast its addition. // This should only be called for a room that is known to contain at least // one player. protected function addMonster (roomId :int) :void {       // Add a monster to the room. var room :RoomSubControlServer = _control.getRoom(roomId); if (_roomMonsters[roomId] == null) { _roomMonsters[roomId] = 1; room.addEventListener(AVRGameRoomEvent.ROOM_UNLOADED, handleRoomUnloaded); _log.info("Adding first monster to room #" + roomId); } else { _roomMonsters[roomId] += 1; _log.info("Adding a monster to room #" + roomId + ". Monsters in room: " + _roomMonsters[roomId]); }

// Set the room-level count. room.props.set(SimpleAVRGConstants.ROOM_MONSTER_COUNT, _roomMonsters[roomId]);

// Store the sum so we don't have to recalculate it. _numMonsters += 1;

// Transmit the new global total. _control.game.props.set(SimpleAVRGConstants.GLOBAL_MONSTER_COUNT, _numMonsters); }   ...

When a room gets its first monster, it also gets a ROOM_UNLOADED listener. The handler for this event removes that room's entry from the _roomMonsters dictionary and from the global monster count.

Server.as ...

// Monsters flee an unloading room. // (Maybe later, we'll have them flee the unloading room into another room   // in the game. For now, they return to the Underwhirled.) protected function handleRoomUnloaded (event :AVRGameRoomEvent) :void {       _log.info("Unloading room #" + event.roomId); if (_roomMonsters[event.roomId] != null) { var removedMonsters :int = _roomMonsters[event.roomId]; delete _roomMonsters[event.roomId]; removeMonsters(removedMonsters); } else { _log.info("No monsters to remove from room #" + event.roomId); }   }

...

We add the new message name as a string constant in the props file.

SimpleAVRGConstants.as
...

public class SimpleAVRGConstants { public static const GLOBAL_MONSTER_COUNT:String = "globalMonsterCount"; public static const ROOM_MONSTER_COUNT:String = "roomMonsterCount"; }

...

The client now needs to listen for these room property changes in addition to the game property changes.

It also needs to listen for the player moving to a new room so that it can update the score board to represent the new room's monster count.

SimpleAVRG.as
...

public function SimpleAVRG {       ...

if (_control.isConnected) { // Set the initial values on the score board. _scoreBoard.setGlobalMonsterCount( int(_control.game.props.get(SimpleAVRGConstants.GLOBAL_MONSTER_COUNT)) ); _scoreBoard.setRoomMonsterCount( int(_control.room.props.get(SimpleAVRGConstants.ROOM_MONSTER_COUNT)) );

// Listen for game level property changes. _control.game.props.addEventListener(PropertyChangedEvent.PROPERTY_CHANGED, handleGamePropertyChanged);

// Listen for room level property changes. _control.room.props.addEventListener(PropertyChangedEvent.PROPERTY_CHANGED, handleRoomPropertyChanged);

// Listen for the player moving into a new room. _control.player.addEventListener(AVRGamePlayerEvent.ENTERED_ROOM, handleEnteredRoom); }   }

...

/**    * This is called when a room property has changed. */   protected function handleRoomPropertyChanged (event :PropertyChangedEvent) :void {       // If the room monster count has changed, update the notice board. if (event.name == SimpleAVRGConstants.ROOM_MONSTER_COUNT) { _scoreBoard.setRoomMonsterCount(event.newValue as int); }   }

/**    * This is called when the player enters a new room, as well as when the * player first enters the game. */   protected function handleEnteredRoom (event :AVRGamePlayerEvent) :void {       var roomCount :Object = _control.room.props.get(SimpleAVRGConstants.ROOM_MONSTER_COUNT); _scoreBoard.setRoomMonsterCount(roomCount == null ? 0 : roomCount as int); }

...

The unload handler should remove these new listeners when the class is unloaded.

SimpleAVRG.as ...

/**    * This is called when your game is unloaded. */   protected function handleUnload (event :Event) :void {       // stop any sounds, clean up any resources that need it. This specifically includes // unregistering listeners to any events - especially Event.ENTER_FRAME if (_control != null) { _control.game.props.removeEventListener(PropertyChangedEvent.PROPERTY_CHANGED, handleGamePropertyChanged); _control.room.props.removeEventListener(PropertyChangedEvent.PROPERTY_CHANGED, handleRoomPropertyChanged); _control.player.removeEventListener(AVRGamePlayerEvent.ENTERED_ROOM, handleEnteredRoom); }   }

...

Adding the new field to the score board is simple.

ScoreBoard.as
...

/**    * Set the display for the number of monsters in the room. */   public function setRoomMonsterCount (monsters :int) :void {       _roomMonsterCountDisplay.text = "Monsters in room: " + monsters; }

...

// Set up the display objects. protected function buildDisplay :void {       ...

_roomMonsterCountDisplay = new TextField; _roomMonsterCountDisplay.autoSize = TextFieldAutoSize.LEFT; _roomMonsterCountDisplay.text = "Monsters in room: 0"; // This will usually be immediately updated. _roomMonsterCountDisplay.x = FIELD_LEFT; _roomMonsterCountDisplay.y = FIELD_TOP + FIELD_HEIGHT; addChild(_roomMonsterCountDisplay); }

...

Source Files
Here is the new state of the source.

ScoreBoard.as
// // $Id$ // // ScoreBoard - A game state data display for SimpleAVRG.

package {

import com.threerings.util.Log; import flash.display.Sprite; import flash.text.TextField; import flash.text.TextFieldAutoSize;

public class ScoreBoard extends Sprite {   public static const WIDTH :int = 150; public static const HEIGHT :int = 100;

// The base X position of the text fields. public static const FIELD_LEFT :int = 5;

// The base Y position and height (including padding) of the text fields. public static const FIELD_TOP :int = 10; public static const FIELD_HEIGHT :int = 15;

/**    * Create a new score board. */   public function ScoreBoard {       buildDisplay; }

/**    * Set the display for the total number of monsters in the game to the given value. */   public function setGlobalMonsterCount (monsters :int) :void {       _globalMonsterCountDisplay.text = "Monsters in game: " + monsters; }

/**    * Set the display for the number of monsters in the room. */   public function setRoomMonsterCount (monsters :int) :void {       _roomMonsterCountDisplay.text = "Monsters in room: " + monsters; }

// Set up the display objects. protected function buildDisplay :void {       graphics.beginFill(0x666666); graphics.drawRoundRect(0, 0, WIDTH, HEIGHT, 3, 3); graphics.endFill;

_globalMonsterCountDisplay = new TextField; _globalMonsterCountDisplay.autoSize = TextFieldAutoSize.LEFT; _globalMonsterCountDisplay.text = "Monsters in game: 0"; // This will usually be immediately updated. _globalMonsterCountDisplay.x = FIELD_LEFT; _globalMonsterCountDisplay.y = FIELD_TOP; addChild(_globalMonsterCountDisplay);

_roomMonsterCountDisplay = new TextField; _roomMonsterCountDisplay.autoSize = TextFieldAutoSize.LEFT; _roomMonsterCountDisplay.text = "Monsters in room: 0"; // This will usually be immediately updated. _roomMonsterCountDisplay.x = FIELD_LEFT; _roomMonsterCountDisplay.y = FIELD_TOP + FIELD_HEIGHT; addChild(_roomMonsterCountDisplay); }

// The display for the total number of monsters in the game. protected var _globalMonsterCountDisplay :TextField;

// The display for the number of monsters in the room. protected var _roomMonsterCountDisplay :TextField;

// The logger. protected var _log :Log = Log.getLog(ScoreBoard); }

}

Server.as
// // $Id$ // // The server agent for SimpleAVRG - an AVR game for Whirled

package {

import com.threerings.util.Log; import com.whirled.ServerObject; import com.whirled.avrg.AVRServerGameControl; import com.whirled.avrg.AVRGameRoomEvent; import com.whirled.avrg.RoomSubControlServer; import flash.events.Event; import flash.events.TimerEvent; import flash.utils.Dictionary; import flash.utils.Timer;

/** * The server agent for SimpleAVRG. Automatically created by the * whirled server whenever a new game is started. */ public class Server extends ServerObject {   /**      * This is the interval at which the game decides whether or not to add * monsters and the odds that it will add a monster for each player it    * considers. */   public const ADD_MONSTER_DELAY :int = 1000;

public const ADD_MONSTER_CHANCE :Number = 0.05;

/**    * Constructs a new server agent. */   public function Server {       _log.info("SimpleAVRG server agent reporting for duty!");

_control = new AVRServerGameControl(this);

// listen for an unload event _control.addEventListener(Event.UNLOAD, handleUnload);

// start with no monsters _numMonsters = 0; _control.game.props.set(SimpleAVRGConstants.GLOBAL_MONSTER_COUNT, 0); _roomMonsters = new Dictionary;

// add monsters periodically _monsterTimer = new Timer(ADD_MONSTER_DELAY); _monsterTimer.addEventListener(TimerEvent.TIMER, maybeAddMonsters); _monsterTimer.start; }

// Consider adding monsters. protected function maybeAddMonsters (event :TimerEvent) :void {       // For each player in the game, consider adding a monster to that // player's location. This means that rooms with more players in them // have greater odds of receiving monsters. for each (var playerId :int in _control.game.getPlayerIds) { if (Math.random <= ADD_MONSTER_CHANCE) { addMonster( _control.getPlayer(playerId).getRoomId ); }       }    }

// Add a monster to a room and broadcast its addition. // This should only be called for a room that is known to contain at least // one player. protected function addMonster (roomId :int) :void {       // Add a monster to the room. var room :RoomSubControlServer = _control.getRoom(roomId); if (_roomMonsters[roomId] == null) { _roomMonsters[roomId] = 1; room.addEventListener(AVRGameRoomEvent.ROOM_UNLOADED, handleRoomUnloaded); _log.info("Adding first monster to room #" + roomId); } else { _roomMonsters[roomId] += 1; _log.info("Adding a monster to room #" + roomId + ". Monsters in room: " + _roomMonsters[roomId]); }

// Set the room-level count. room.props.set(SimpleAVRGConstants.ROOM_MONSTER_COUNT, _roomMonsters[roomId]);

// Store the sum so we don't have to recalculate it. _numMonsters += 1;

// Transmit the new global total. _control.game.props.set(SimpleAVRGConstants.GLOBAL_MONSTER_COUNT, _numMonsters); }

// Remove a number of monsters from the game and report the removal. protected function removeMonsters (removedMonsters :int) :void {       _log.info("Removing " + removedMonsters + " monsters from global count."); _numMonsters -= removedMonsters; _control.game.props.set(SimpleAVRGConstants.GLOBAL_MONSTER_COUNT, _numMonsters); }

// Monsters flee an unloading room. // (Maybe later, we'll have them flee the unloading room into another room   // in the game. For now, they return to the Underwhirled.) protected function handleRoomUnloaded (event :AVRGameRoomEvent) :void {       _log.info("Unloading room #" + event.roomId); if (_roomMonsters[event.roomId] != null) { var removedMonsters :int = _roomMonsters[event.roomId]; delete _roomMonsters[event.roomId]; removeMonsters(removedMonsters); } else { _log.info("No monsters to remove from room #" + event.roomId); }   }

// Don't pollute! Unload your resources. protected function handleUnload (event :Event) :void {       if (_monsterTimer != null) { _monsterTimer.stop; _monsterTimer.removeEventListener(TimerEvent.TIMER, maybeAddMonsters); }   }

// A timer for periodically adding monsters. protected var _monsterTimer :Timer;

// This is the total number of monsters in the whole game. protected var _numMonsters :int = 0;

// This is the number of monsters in each room. protected var _roomMonsters :Dictionary;

// The server side game control. protected var _control :AVRServerGameControl;

// The logger. protected var _log :Log = Log.getLog(Server); }

}

SimpleAVRG.as
// // $Id$ // // SimpleAVRG - an AVR game for Whirled

package {

import com.threerings.util.Log; import com.whirled.avrg.AVRGameControl; import com.whirled.avrg.AVRGamePlayerEvent; import com.whirled.net.PropertyChangedEvent; import flash.display.Sprite; import flash.events.Event;

public class SimpleAVRG extends Sprite {   public function SimpleAVRG {       _control = new AVRGameControl(this);

// Set up the display. buildDisplay;

// listen for an unload event _control.addEventListener(Event.UNLOAD, handleUnload);

if (_control.isConnected) { // Set the initial values on the score board. _scoreBoard.setGlobalMonsterCount( int(_control.game.props.get(SimpleAVRGConstants.GLOBAL_MONSTER_COUNT)) ); _scoreBoard.setRoomMonsterCount( int(_control.room.props.get(SimpleAVRGConstants.ROOM_MONSTER_COUNT)) );

// Listen for game level property changes. _control.game.props.addEventListener(PropertyChangedEvent.PROPERTY_CHANGED, handleGamePropertyChanged);

// Listen for room level property changes. _control.room.props.addEventListener(PropertyChangedEvent.PROPERTY_CHANGED, handleRoomPropertyChanged);

// Listen for the player moving into a new room. _control.player.addEventListener(AVRGamePlayerEvent.ENTERED_ROOM, handleEnteredRoom); }   }

/**    * Build the display. */   protected function buildDisplay :void {       _scoreBoard = new ScoreBoard; _scoreBoard.x = 0; _scoreBoard.y = 0; addChild(_scoreBoard); }

/**    * This is called when a game property has changed. */   protected function handleGamePropertyChanged (event :PropertyChangedEvent) :void {       // If the global monster count has changed, update the notice board. if (event.name == SimpleAVRGConstants.GLOBAL_MONSTER_COUNT) { _scoreBoard.setGlobalMonsterCount(event.newValue as int); }   }

/**    * This is called when a room property has changed. */   protected function handleRoomPropertyChanged (event :PropertyChangedEvent) :void {       // If the room monster count has changed, update the notice board. if (event.name == SimpleAVRGConstants.ROOM_MONSTER_COUNT) { _scoreBoard.setRoomMonsterCount(event.newValue as int); }   }

/**    * This is called when the player enters a new room, as well as when the * player first enters the game. */   protected function handleEnteredRoom (event :AVRGamePlayerEvent) :void {       var roomCount :Object = _control.room.props.get(SimpleAVRGConstants.ROOM_MONSTER_COUNT); _scoreBoard.setRoomMonsterCount(roomCount == null ? 0 : roomCount as int); }

/**    * This is called when your game is unloaded. */   protected function handleUnload (event :Event) :void {       // stop any sounds, clean up any resources that need it. This specifically includes // unregistering listeners to any events - especially Event.ENTER_FRAME if (_control != null) { _control.game.props.removeEventListener(PropertyChangedEvent.PROPERTY_CHANGED, handleGamePropertyChanged); _control.room.props.removeEventListener(PropertyChangedEvent.PROPERTY_CHANGED, handleRoomPropertyChanged); _control.player.removeEventListener(AVRGamePlayerEvent.ENTERED_ROOM, handleEnteredRoom); }   }

protected var _control :AVRGameControl; protected var _scoreBoard :ScoreBoard; protected var _log :Log = Log.getLog(SimpleAVRG); } }

SimpleAVRGConstants.as
// // $Id$ // // The property names for SimpleAVRG

package {

public class SimpleAVRGConstants { public static const GLOBAL_MONSTER_COUNT:String = "globalMonsterCount"; public static const ROOM_MONSTER_COUNT:String = "roomMonsterCount"; }

}

What have you done?
You have added a room level property and client side listeners for that property. You also added lisnteners and handlers for other room related events such as the player switching rooms.

If you build and upload the results, you'll get something like this:


 * Return to the previous step
 * Proceed to the next step