SimpleAVRG/Part 4

This is part four of the SimpleAVRG tutorial. It details the Player 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 Player Monster Counter
This time, we'll add a basket for the player and the ability to catch monsters from the room and put them in the basket. For now, the basket will be an integer stored in a dictionary in Server for convenience and shared with the clients as a value in the player property space.

We'll add listeners for players joining and quitting the game. When a player joins the game, a basket is set up for him. When he leaves, the basket is taken away.

Player requests to catch monsters are going to come to the server as game messages, so we also add a listener and handler for MESSAGE_RECEIVED.

Server.as
...

public function Server {       ...        // listen for players joining and quitting the game _control.game.addEventListener(AVRGameControlEvent.PLAYER_JOINED_GAME, handlePlayerJoinedGame); _control.game.addEventListener(AVRGameControlEvent.PLAYER_QUIT_GAME, handlePlayerQuitGame);

...

// This dictionary will store the count of monsters in each player's basket. _basketMonsters = new Dictionary;

...

// listen for game-level messages _control.game.addEventListener(MessageReceivedEvent.MESSAGE_RECEIVED, handleGameMessageReceived); }

...

// This is the number of monsters in each player's basket. protected var _basketMonsters :Dictionary;

...

The player joined and player left handlers are simple. They add and delete a dictionary entry, respectively.

Server.as ...

// This is called when a player joins the game. protected function handlePlayerJoinedGame (event :AVRGameControlEvent) :void {       // Give the player a basket. _basketMonsters[event.value] = 0; }

// This is called when a player quits the game. protected function handlePlayerQuitGame (event :AVRGameControlEvent) :void {       // Take the player's basket away. delete _basketMonsters[event.value]; }

...

The message received handler does more. For now, it is only interested in client requests to catch monsters from a room and add them to a player's basket.

It gleans the player and room ids from the event and determines whether it is possible for that player to catch a monster from that room. If it is, it decrements the room and global player counts, increments the player's basket count, and updates the appropriate game, room and player properties to let the clients know about the changes.

Server.as ...

// This is called when a game message has been received. protected function handleGameMessageReceived (event :MessageReceivedEvent) :void {       if (event.name == SimpleAVRGConstants.CATCH_MONSTER_REQUEST_MESSAGE) { // Find the player catching a monster and the room from which it is being caught. var playerId :int = event.senderId; var roomId :int = _control.getPlayer(event.senderId).getRoomId;

_log.info("Player #" + playerId + " is attempting to take a monster from room #" + roomId + "."); var monsters :int = int(_roomMonsters[roomId]); if (monsters > 0) { // Set the room-level count. _roomMonsters[roomId] -= 1; _control.getRoom(roomId).props.set(SimpleAVRGConstants.ROOM_MONSTER_COUNT, _roomMonsters[roomId]);

// Set the global count. removeGlobalMonsters(1);

// Set the player's basket count. _basketMonsters[playerId] += 1; _control.getPlayer(playerId).props.set(SimpleAVRGConstants.BASKET_MONSTER_COUNT, _basketMonsters[playerId]); } else { _log.warning("Attempted to remove a monster from empty room #" + roomId + "."); }       }    }

...

The SimpleAVRGConstants class has two new constants to represent the new property and the new message.

SimpleAVRGConstants.as
...

public class SimpleAVRGConstants { ...       public static const BASKET_MONSTER_COUNT :String = "basketMonsterCount";

public static const CATCH_MONSTER_REQUEST_MESSAGE :String = "catchMonsterRequestMessage"; }

...

The client will need a way to display the contents of the player's basket and a way for the player to catch monsters, so the ScoreBoard needs some work.

There are enough nearly identical text fields being set up to make it worth factoring out some common code there, so buildDisplay gets the treatment. It also gets the new basket counter display and a button with a click handler.

ScoreBoard.as
...

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

_globalMonsterCountDisplay = new TextField; initCounterDisplay(_globalMonsterCountDisplay, "Monsters in game");

_roomMonsterCountDisplay = new TextField; initCounterDisplay(_roomMonsterCountDisplay, "Monsters in room");

_basketMonsterCountDisplay = new TextField; initCounterDisplay(_basketMonsterCountDisplay, "Monsters in basket");

_catchMonsterButton = new SimpleTextButton("Catch a Monster"); _catchMonsterButton.addEventListener(MouseEvent.CLICK, handleCatchMonsterClick); _catchMonsterButton.x = FIELD_LEFT; _catchMonsterButton.y = FIELD_TOP + FIELD_HEIGHT * _countDisplaysAdded; addChild(_catchMonsterButton); }

// Set the label and position of a counter display. protected function initCounterDisplay (field :TextField, label :String) :void {       field.autoSize = TextFieldAutoSize.LEFT; field.text = label + ": 0"; // This will usually be immediately updated. field.x = FIELD_LEFT; field.y = FIELD_TOP + FIELD_HEIGHT * _countDisplaysAdded++; addChild(field); }

...

// The display for the number of monsters in the player's basket. protected var _basketMonsterCountDisplay :TextField;

// A counter of the number of text fields added so far. protected var _countDisplaysAdded :int = 0;

// A button for catching monsters. protected var _catchMonsterButton :SimpleTextButton;

...

The new field also gets a setter.

ScoreBoard.as ...

/**    * Set the display for the number of monsters in the player's basket. */   public function setBasketMonsterCount (monsters :int) :void {       _basketMonsterCountDisplay.text = "Monsters in basket: " + monsters; }

...

The score board doesn't have access to game controller, so it can't directly send a monster catching request to the server. Instead, it emits a custom CATCH_MONSTER_REQUEST event which the main client controller class (SimpleAVRG) will handle.

ScoreBoard indirectly extends the EventDispatcher class, so no special work is needed to allow it to dispatch events.

ScoreBoard.as ...

// This is called when the "catch a monster" button has been clicked. protected function handleCatchMonsterClick (event :MouseEvent) :void {       dispatchEvent( new ScoreBoardEvent(ScoreBoardEvent.CATCH_MONSTER_REQUEST) ); }

...

The event is of the new ScoreBoardEvent class, a simple child of Event.

ScoreBoardEvent.as
...

package {

import flash.events.Event;

public class ScoreBoardEvent extends Event { public static const CATCH_MONSTER_REQUEST :String = "catchMonsterRequestEvent";

public function ScoreBoardEvent(type:String) { super(type); }   }

}

The client will need to listen for player property changes now to keep the player's basket up to date.

SimpleAVRG.as
...

public function SimpleAVRG {       ...

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

...   }

...

/**    * This is called when a player property has changed. */   protected function handlePlayerPropertyChanged (event :PropertyChangedEvent) :void {       // If the player's basket monster count has changed, update the notice board. if (event.name == SimpleAVRGConstants.BASKET_MONSTER_COUNT) { _scoreBoard.setBasketMonsterCount( int(event.newValue) ); }   }

...

It will also need to listen for events from the score board, which is now capable of expressing a player's request to catch a monster.

SimpleAVRG.as ...

protected function buildDisplay :void {       _scoreBoard = new ScoreBoard;

...

_scoreBoard.addEventListener(ScoreBoardEvent.CATCH_MONSTER_REQUEST, handleCatchMonsterRequest); }

...

When such a request is detected, the class notes it in the client-side log and sends the request on to the game agent as a message, which will be handled by Server.handleGameMessageReceived as described above.

SimpleAVRG.as ...

/**    * This is called when the player is attempting to catch a monster. */   protected function handleCatchMonsterRequest (event :ScoreBoardEvent) :void {       _log.info("Player attempted to catch a monster."); _control.agent.sendMessage(SimpleAVRGConstants.CATCH_MONSTER_REQUEST_MESSAGE); }

...

Source Files
The source now looks like this:

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

package {

import com.threerings.ui.SimpleTextButton; import com.threerings.util.Log; import flash.display.Sprite; import flash.events.MouseEvent; 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 the display for the number of monsters in the player's basket. */   public function setBasketMonsterCount (monsters :int) :void {       _basketMonsterCountDisplay.text = "Monsters in basket: " + 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; initCounterDisplay(_globalMonsterCountDisplay, "Monsters in game");

_roomMonsterCountDisplay = new TextField; initCounterDisplay(_roomMonsterCountDisplay, "Monsters in room");

_basketMonsterCountDisplay = new TextField; initCounterDisplay(_basketMonsterCountDisplay, "Monsters in basket");

_catchMonsterButton = new SimpleTextButton("Catch a Monster"); _catchMonsterButton.addEventListener(MouseEvent.CLICK, handleCatchMonsterClick); _catchMonsterButton.x = FIELD_LEFT; _catchMonsterButton.y = FIELD_TOP + FIELD_HEIGHT * _countDisplaysAdded; addChild(_catchMonsterButton); }

// Set the label and position of a counter display. protected function initCounterDisplay (field :TextField, label :String) :void {       field.autoSize = TextFieldAutoSize.LEFT; field.text = label + ": 0"; // This will usually be immediately updated. field.x = FIELD_LEFT; field.y = FIELD_TOP + FIELD_HEIGHT * _countDisplaysAdded++; addChild(field); }

// This is called when the "catch a monster" button has been clicked. protected function handleCatchMonsterClick (event :MouseEvent) :void {       dispatchEvent( new ScoreBoardEvent(ScoreBoardEvent.CATCH_MONSTER_REQUEST) ); }

// 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 display for the number of monsters in the player's basket. protected var _basketMonsterCountDisplay :TextField;

// A counter of the number of text fields added so far. protected var _countDisplaysAdded :int = 0;

// A button for catching monsters. protected var _catchMonsterButton :SimpleTextButton;

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

}

ScoreBoardEvent.as
// // $Id$ // // An event generated by the ScoreBoard.

package {

import flash.events.Event;

public class ScoreBoardEvent extends Event { public static const CATCH_MONSTER_REQUEST :String = "catchMonsterRequestEvent";

public function ScoreBoardEvent(type:String) { super(type); }   }

}

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.AVRGameControlEvent; import com.whirled.avrg.AVRGameRoomEvent; import com.whirled.avrg.RoomSubControlServer; import com.whirled.net.MessageReceivedEvent; 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);

// listen for players joining and quitting the game _control.game.addEventListener(AVRGameControlEvent.PLAYER_JOINED_GAME, handlePlayerJoinedGame); _control.game.addEventListener(AVRGameControlEvent.PLAYER_QUIT_GAME, handlePlayerQuitGame);

// start with no monsters _numMonsters = 0; _control.game.props.set(SimpleAVRGConstants.GLOBAL_MONSTER_COUNT, 0); // This dictionary will store the count of monsters in each room. _roomMonsters = new Dictionary;

// This dictionary will store the count of monsters in each player's basket. _basketMonsters = new Dictionary;

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

// listen for game-level messages _control.game.addEventListener(MessageReceivedEvent.MESSAGE_RECEIVED, handleGameMessageReceived); }

// 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 removeGlobalMonsters (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]; removeGlobalMonsters(removedMonsters); } else { _log.info("No monsters to remove from room #" + event.roomId); }   }

// This is called when a player joins the game. protected function handlePlayerJoinedGame (event :AVRGameControlEvent) :void {       // Give the player a basket. _basketMonsters[event.value] = 0; }

// This is called when a player quits the game. protected function handlePlayerQuitGame (event :AVRGameControlEvent) :void {       // Take the player's basket away. delete _basketMonsters[event.value]; }

// This is called when a game message has been received. protected function handleGameMessageReceived (event :MessageReceivedEvent) :void {       if (event.name == SimpleAVRGConstants.CATCH_MONSTER_REQUEST_MESSAGE) { // Find the player catching a monster and the room from which it is being caught. var playerId :int = event.senderId; var roomId :int = _control.getPlayer(event.senderId).getRoomId;

_log.info("Player #" + playerId + " is attempting to take a monster from room #" + roomId + "."); var monsters :int = int(_roomMonsters[roomId]); if (monsters > 0) { // Set the room-level count. _roomMonsters[roomId] -= 1; _control.getRoom(roomId).props.set(SimpleAVRGConstants.ROOM_MONSTER_COUNT, _roomMonsters[roomId]);

// Set the global count. removeGlobalMonsters(1);

// Set the player's basket count. _basketMonsters[playerId] += 1; _control.getPlayer(playerId).props.set(SimpleAVRGConstants.BASKET_MONSTER_COUNT, _basketMonsters[playerId]); } else { _log.warning("Attempted to remove a monster from empty room #" + 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;

// This is the number of monsters in each player's basket. protected var _basketMonsters :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 player level property changes. _control.player.props.addEventListener(PropertyChangedEvent.PROPERTY_CHANGED, handlePlayerPropertyChanged);

// 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);

_scoreBoard.addEventListener(ScoreBoardEvent.CATCH_MONSTER_REQUEST, handleCatchMonsterRequest); }

/**    * 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( int(event.newValue) ); }   }

/**    * 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( int(event.newValue )); }   }

/**    * This is called when a player property has changed. */   protected function handlePlayerPropertyChanged (event :PropertyChangedEvent) :void {       // If the player's basket monster count has changed, update the notice board. if (event.name == SimpleAVRGConstants.BASKET_MONSTER_COUNT) { _scoreBoard.setBasketMonsterCount( int(event.newValue) ); }   }

/**    * 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 the player is attempting to catch a monster. */   protected function handleCatchMonsterRequest (event :ScoreBoardEvent) :void {       _log.info("Player attempted to catch a monster."); _control.agent.sendMessage(SimpleAVRGConstants.CATCH_MONSTER_REQUEST_MESSAGE); }

/**    * 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 and message names for SimpleAVRG

package {

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

public static const CATCH_MONSTER_REQUEST_MESSAGE :String = "catchMonsterRequestMessage"; }

}

What have you done?
You have added a player level property and client side listeners for that property. You have also added a way--though perhaps not a terribly exciting way--for the player to interact with the game.

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


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