An Introduction to ElectroServer and PushButton Engine: Part Two

31
Mar/10
1

In part one, the focus was on building the server logic for a multiplayer game. Now, we’ll get to work on a PushButton Engine client that will connect and communicate with ElectroServer. If you’re completely unaware of how PushButton Engine works, I’d urge you to spend some time checking out the documentation to get yourself up to speed.

Let’s get started!

The Client

The client we’re going to build is a fairly primitive one. It will connect to ElectroServer, spawn a knight player avatar and display anyone else that is connected as a black wizard. As players move around the area, their position will be broadcast across all connected clients and updated appropriately.

PushButton Engine lets the developer declare which objects they’d to use via an XML file (typically referred to as a ‘level file’). For the application developers among you, it will feel a lot like some of the Inversion of Control solutions that are available today. We’ll create one file that will declare common entities that are relevant across various game states and another that represents the primary game mode.

Defining the Game

File: /ESPBEDemo-client/assets/levels/common.xml

<?xml version="1.0" encoding="utf-8"?>

<!-- Define all the entities that will be used across game modes. -->

<things version="1">

    <!-- We need a spatial manager entity component to keep track of entities
    that have a spatial component and thus require a position be set. Like our
    soon-to-be-created avatars. -->
    <entity name="spatialManager">
        <component type="com.pblabs.rendering2D.BasicSpatialManager2D" name="manager" />
    </entity>

    <!-- Create a display object scene that will do the rendering for us. -->
    <entity name="scene">
        <component type="com.pblabs.rendering2D.DisplayObjectScene" name="scene">
            <!-- Assign it a name -->
            <sceneViewName>sceneView</sceneViewName>
            <!-- and set the scene position. -->
            <position>
                <x>-400</x>
                <y>-300</y>
            </position>
        </component>
    </entity>

    <!-- Now add these two entities to a group so they can be instantiated conveniently. -->
    <group name="common">
        <objectReference name="spatialManager" />
        <objectReference name="scene" />
      </group>

</things>

We now have a spatial manager to handle the positioning of objects on the screen and a scene to render them to. Now let’s define the object we’ll need when our main game mode (or level) is loaded.

File: /ESPBEDemo-client/assets/levels/game.xml

<?xml version="1.0" encoding="utf-8"?>

<!-- Define all the objects that need to be available when this level is loaded -->
<things version="1">

    <!-- Create the player avatar. -->
    <template name="player">
        <!-- Create a sprite renderer to render the player avatar -->
        <component type="com.pblabs.rendering2D.SpriteRenderer" name="sprite">
            <!-- Tell the sprite on what scene it should be rendered.
            (The 'scene' entity we created in common.xml.) -->
            <scene entityName="scene" componentName="scene"/>
            <!-- Define the location of the image file. -->
            <fileName>../assets/images/player.png</fileName>
            <!-- Assign the layer index. -->
            <layerIndex>1</layerIndex>
            <!-- Define where this sprite should be rendered. -->
            <positionProperty>@spatial.position</positionProperty>
        </component>
        <!-- This component allows the entity to be positioned on the screen. -->
        <component type="com.pblabs.rendering2D.SimpleSpatialComponent" name="spatial">
            <!-- Make sure it knows where the spatial manager is (from common.xml). -->
            <spatialManager entityName="spatialManager" />
            <!-- And how large it should be. -->
            <size>
                <x>48</x>
                <y>48</y>
            </size>
        </component>
        <!-- Add a keyboard input component to accept user input. -->
        <component type="com.philperon.espbedemo.components.KeyboardInput" name="keyboardInput">
            <!-- Assign W, A, S and D for player movement. -->
            <inputMap childType="com.pblabs.engine.core.InputKey">
                <up>W</up>
                <down>S</down>
                <left>A</left>
                <right>D</right>
            </inputMap>
        </component>
        <!-- Add a controller component to deal with how user input should be handled. -->
        <component type="com.philperon.espbedemo.components.PlayerController" name="controller">
            <keyboardInput componentName="keyboardInput" />
            <spatial componentName="spatial" />
        </component>
    </template>

    <!-- Create the opponent avatar. -->
    <template name="opponent">
        <!-- Similar setup to that shown above. -->
        <component type="com.pblabs.rendering2D.SpriteRenderer" name="sprite">
            <scene entityName="scene" componentName="scene"/>
            <fileName>../assets/images/opponent.png</fileName>
            <layerIndex>1</layerIndex>
            <positionProperty>@mover.position</positionProperty>
        </component>
        <!-- Add a component to handle interpolated movement based on position
        updates from the server. -->
        <component type="com.pblabs.rendering2D.Interpolated2DMoverComponent" name="mover">
            <!-- How many degrees should be of the goal position before movement begins. -->
            <movementHeadingThreshold>360</movementHeadingThreshold>
            <!-- Assign the spatial manager. -->
            <spatialManager entityName="spatialManager" />
            <!-- Tweak the movement speed slightly to give it a more natural look. -->
            <nudge>1.2</nudge>
            <!-- Like the player avatar, define the size. -->
            <size>
                <x>48</x>
                <y>48</y>
            </size>
        </component>
    </template>

</things>

The game.xml file really starts to illustrate how powerful PushButton Engine can be once you wrap your head around a composition-style architecture. By writing small components that have a single primary duty, you can reuse them again and again in an unlimited number of entities. Game assembly then becomes a simple matter of defining the “recipe” that makes your game unique. It also opens up opportunities for game designers to have a hand in game development with only a limited knowledge of ActionScript.

Another group of items being referenced are the actual assets themselves. For this demonstration this includes the .xml level files and .png game graphics. We’ll place these in a group so they can be embedded and easily handled when the game initializes.

File: /ESPBEDemo-client/src/com/philperon/espbedemo/Resources.as

package com.philperon.espbedemo
{
    import com.pblabs.engine.PBE;
    import com.pblabs.engine.resource.ImageResource;
    import com.pblabs.engine.resource.ResourceBundle;
    import com.pblabs.engine.resource.XMLResource;

    import flash.display.Bitmap;
    import flash.utils.ByteArray;

    // Create a new resouce bundle to handle loading and registering
    // embedded resources.
    public class Resources extends ResourceBundle
    {
        // Add the level files we'll need.
        [Embed(source="../assets/levels/common.xml", mimeType="application/octet-stream")]
        public var Common:Class;

        [Embed(source="../assets/levels/game.xml", mimeType="application/octet-stream")]
        public var Game:Class;

        // And the graphics used to represent the game.
        [Embed(source="../assets/images/background.png")]
        public var BackgroundPNG:Class;

        [Embed(source="../assets/images/player.png")]
        public var PlayerPNG:Class;

        [Embed(source="../assets/images/opponent.png")]
        public var OpponentPNG:Class;

        public function Resources()
        {
            // When this class is instantiated, tell the resource manager to register the resources listed above.
            PBE.resourceManager.registerEmbeddedResource("../assets/levels/common.xml", XMLResource, new Common() as ByteArray);
            PBE.resourceManager.registerEmbeddedResource("../assets/levels/game.xml", XMLResource, new Game() as ByteArray);
            PBE.resourceManager.registerEmbeddedResource("../assets/images/background.png", ImageResource, new BackgroundPNG() as Bitmap);
            PBE.resourceManager.registerEmbeddedResource("../assets/images/player.png", ImageResource, new PlayerPNG() as Bitmap);
            PBE.resourceManager.registerEmbeddedResource("../assets/images/opponent.png", ImageResource, new OpponentPNG() as Bitmap);
        }
    }
}

Now we have all the assets we’ll need to display our game. It’s time to start filling in some of the actual game logic that typically resides inside components. If you recall, we referenced a component to handle keyboard input and another to deal with how that input should be handled. Let’s start with our KeyboardInput component.

File: /ESPBEDemo-client/src/com/philperon/espbedemo/components/KeyboardInput.as

package com.philperon.espbedemo.components
{
    import com.pblabs.engine.core.InputMap;
    import com.pblabs.engine.entity.EntityComponent;

    // A component that handles keyboard input!
    public class KeyboardInput extends EntityComponent
    {
        // Define some variables to store keyboard input values.
        public var up:Number = 0.0;
        public var down:Number = 0.0;
        public var left:Number = 0.0;
        public var right:Number = 0.0;

        // getter and setter for the InputMap class that does the
        // real work here.
        public function get inputMap():InputMap
        {
            return _inputMap;
        }

        public function set inputMap(map:InputMap):void
        {
            // Store the input map in a private variable...
            _inputMap = map;

            // ...and map the actions we defined in game.xml to
            // the handlers.
            if (_inputMap != null)
            {
                _inputMap.mapActionToHandler("up", upHandler);
                _inputMap.mapActionToHandler("down", downHandler);
                _inputMap.mapActionToHandler("left", leftHandler);
                _inputMap.mapActionToHandler("right", rightHandler);
            }
        }

        // Finally, set up the handlers themselves.
        private function upHandler(value:Number):void
        {
            up = value;
        }

        private function downHandler(value:Number):void
        {
            down = value;
        }

        private function leftHandler(value:Number):void
        {
            left = value;
        }

        private function rightHandler(value:Number):void
        {
            right = value;
        }

        private var _inputMap:InputMap;
    }
}

The game can now respond to keyboard input. We’re getting close to the core of our demo but need some way to ensure all these great resources make it on to the display list. For this purpose, we’ll use a subclass of BaseScreen that will be registered with a screen manager once we dive into the main game class.

File: /ESPBEDemo-client/src/com/philperon/espbedemo/GameScreen.as

package com.philperon.espbedemo
{
    import com.pblabs.engine.PBE;
    import com.pblabs.engine.debug.Logger;
    import com.pblabs.engine.resource.ImageResource;
    import com.pblabs.rendering2D.ui.SceneView;
    import com.pblabs.screens.BaseScreen;

    // Create a new screen class that will be registered with the
    // screen manager.
    public class GameScreen extends BaseScreen
    {
        // Declare a scene variable so we have something to actually
        // draw to.
        public var sceneView:SceneView;

        public function GameScreen()
        {
            super();

            // Create the scene view to match the same size as the .swf
            // and add to the display list.
            sceneView = new SceneView();
            sceneView.name = "sceneView";
            sceneView.width = 800;
            sceneView.height = 600;
            addChild(sceneView);

            // Request that a background image be loaded on to the screen.
            // Pass in handlers to deal with a success or failure.
            PBE.resourceManager.load("../assets/images/background.png", ImageResource, imageLoadSuccessHandler, imageLoadFailedHandler);
        }

        // Once the image is loaded, send the bitmap data into a fill on
        // the Graphics instance of this display object.
        private function imageLoadSuccessHandler(imageResource:ImageResource):void
        {
            cacheAsBitmap = true;
            graphics.clear();
            graphics.beginBitmapFill(imageResource.image.bitmapData);
            graphics.drawRect(0, 0, imageResource.image.bitmapData.width, imageResource.image.bitmapData.height);
            graphics.endFill();
        }

        private function imageLoadFailedHandler(imageResource:ImageResource):void
        {
            Logger.print(this, "Failed to load image '" + imageResource.filename + "'");
        }
    }
}

We’re getting close. It’s time to wire up the PlayerController we added to the player entity in the game.xml file. This will contain all the logic needed to control the player’s avatar including sending the position updates to ElectroServer. Here we go…

File: /ESPBEDemo-client/src/com/philperon/espbedemo/components/PlayerController.as

package com.philperon.espbedemo.components
{
    import com.electrotank.electroserver4.ElectroServer;
    import com.electrotank.electroserver4.esobject.EsObject;
    import com.electrotank.electroserver4.message.request.PluginRequest;
    import com.pblabs.engine.PBE;
    import com.pblabs.engine.components.TickedComponent;
    import com.pblabs.rendering2D.SimpleSpatialComponent;

    import flash.geom.Point;

    // This represents the core logic for a player avatar.
    // It extends TickedComponent so we have access to game ticks.
    public class PlayerController extends TickedComponent
    {
        // Set how fast we'd like the player to move.
        public static const SPEED:uint = 100;

        // Declare a variable to hold the ElectroServer connection.
        public var connection:ElectroServer;
        // Make sure the player knows the current room and zone ids.
        public var currentRoomID:Number;
        public var currentZoneID:Number;
        // Create a handle to the KeyboardInput component we just created.
        public var keyboardInput:KeyboardInput;
        // As mentioned earlier, the spatial component manages position.
        public var spatial:SimpleSpatialComponent;
        // And of course, every player has a name.
        public var username:String;

        // The position buffer will ensure we're not sending updates if
        // the player hasn't moved.
        private var _positionBuffer:Point = new Point();

        public function PlayerController()
        {
            super();
        }

        // The onTick method that gets called every game tick.
        override public function onTick(deltaTime:Number):void
        {
            // Set the x and y velocity based on the keyboard input registered
            // from the KeyboardInput component.
            spatial.velocity.x = (keyboardInput.right - keyboardInput.left) * PlayerController.SPEED;
            spatial.velocity.y = (keyboardInput.down - keyboardInput.up) * PlayerController.SPEED;

            // Bounds check the player's movement. (Ideally, this would also be
            // enforced on the server but I'm trying to keep things as
            // simple as possible.)
            if(spatial.position.x < 0 && spatial.velocity.x < 0 ||
                spatial.position.x > 800 && spatial.velocity.x > 0)
                spatial.velocity.x = 0;

            if(spatial.position.y < 0 && spatial.velocity.y < 0 ||
                spatial.position.y > 600 && spatial.velocity.y > 0)
                spatial.velocity.y = 0;
        }

        // This fires when this component is added to an entity.
        override protected function onAdd():void
        {
            super.onAdd();
            // Schedule a position update task with the process manager.
            PBE.processManager.schedule(200, this, sendPosition);
        }

        // The function that's called when the task we just scheduled is due.
        public function sendPosition():void
        {
            // Check to make sure we've actually moved.
            if((spatial.x != _positionBuffer.x) || (spatial.y != _positionBuffer.y))
            {
                // If we have moved, create a new request to be sent back to the server
                var pr:PluginRequest = new PluginRequest();
                // Set the plugin name as well as the room and zone id
                // that we created in Part 1 of this tutorial.
                pr.setPluginName("GamePlugin");
                pr.setRoomId(currentRoomID);
                pr.setZoneId(currentZoneID);

                // Create an EsObject that will act as our message payload.
                var pos:EsObject = new EsObject();
                // Set the opcode (also found in Part 1)
                pos.setInteger("opcode", ESPBEDemo.STATE_UPDATE);
                // Make sure the server knows who it's coming from...
                pos.setString("name", username);
                // ...and what we're changing.
                pos.setNumber("x", spatial.x);
                pos.setNumber("y", spatial.y);
                // Add the payload to our plugin request.
                pr.setEsObject(pos);
                // Now send it on it's way.
                connection.send(pr);                

                // Push our current position into the buffer.
                _positionBuffer.x = spatial.x;
                _positionBuffer.y = spatial.y;
            }
            // Finally, schedule this task to run again.
            PBE.processManager.schedule(200, this, sendPosition);
        }
    }
}

All the support classes are finished. Now to focus on the main game class that will set up the server connection and start our client.

A Series of Events

Much of what we’ll be looking at within our main game class is driven by events. Both PushButton Engine and the ElectroServer AS3 API expose events that help you drive your game logic. For a large project you may not want to bundle all your handlers in one place, but we do so here to make it easier to follow how the logic flows from game initialization, server connection and finally gameplay.

File: /ESPBEDemo-client/src/ESPBEDemo.as

package
{
    import com.electrotank.electroserver4.ElectroServer;
    import com.electrotank.electroserver4.entities.Protocol;
    import com.electrotank.electroserver4.entities.SearchCriteria;
    import com.electrotank.electroserver4.entities.UserVariable;
    import com.electrotank.electroserver4.esobject.EsObject;
    import com.electrotank.electroserver4.message.MessageType;
    import com.electrotank.electroserver4.message.event.ConnectionEvent;
    import com.electrotank.electroserver4.message.event.PluginMessageEvent;
    import com.electrotank.electroserver4.message.request.LoginRequest;
    import com.electrotank.electroserver4.message.request.PluginRequest;
    import com.electrotank.electroserver4.message.request.QuickJoinGameRequest;
    import com.electrotank.electroserver4.message.response.CreateOrJoinGameResponse;
    import com.electrotank.electroserver4.message.response.LoginResponse;
    import com.pblabs.engine.PBE;
    import com.pblabs.engine.core.TemplateManager;
    import com.pblabs.engine.entity.IEntity;
    import com.pblabs.engine.entity.PropertyReference;
    import com.pblabs.rendering2D.*;
    import com.pblabs.rendering2D.spritesheet.SpriteSheetComponent;
    import com.philperon.espbedemo.*;
    import com.philperon.espbedemo.components.*;

    import flash.display.Sprite;
    import flash.display.StageScaleMode;
    import flash.events.Event;
    import flash.geom.Point;

    // Set up our swf configuration
    [SWF(width="800", height="600", frameRate="60", backgroundColor="0x000000")]
    public class ESPBEDemo extends Sprite
    {
        // Define our game protocol here. This reflects the same values we set in
        // part one.
        public static const PLAYER_INIT:int = 1;
        public static const OPPONENT_ENTERED:int = 2;
        public static const STATE_UPDATE:int = 3;
        public static const PLAYER_LEFT:int = 4;

        // declare variable for the ElectroServer connection.
        private var _es:ElectroServer;
        // The room and zone ids.
        private var _roomID:Number = -1;
        private var _zoneID:Number = -1;
        // General properties on users.
        private var _userProps:EsObject;
        // The player name
        private var _name:String;
        // And a collection to hold opponents objects.
        private var _opponents:Object = {};
        // This will hold the type of game we're assigned to.
        private var _gameType:String;

        public function ESPBEDemo()
        {
            // Set the scale mode.
            stage.scaleMode = StageScaleMode.SHOW_ALL;

            // Register classes that we reference in our level files here.
            // This is so the compiler knows to include them.
            PBE.registerType(DisplayObjectRenderer);
            PBE.registerType(DisplayObjectScene);
            PBE.registerType(Interpolated2DMoverComponent);
            PBE.registerType(SimpleSpatialComponent);
            PBE.registerType(SpriteSheetComponent);
            PBE.registerType(SpriteRenderer);
            PBE.registerType(PlayerController);
            PBE.registerType(KeyboardInput);

            // Start PushButton Engine.
            PBE.startup(this);

            // Add our resources.
            PBE.addResources(new Resources());

            // And register the game screen we created earlier.
            PBE.screenManager.registerScreen("game", new GameScreen());

            // Set up a loaded handler so that we know when our common.xml file is ready to go.
            PBE.templateManager.addEventListener(TemplateManager.LOADED_EVENT, loadedHandler);
            PBE.templateManager.loadFile("../assets/levels/common.xml");
        }

        // The loadedHandler fires once the common.xml level file is loaded.
        private function loadedHandler(event:Event):void
        {
            // Clean up the listener.
            PBE.templateManager.removeEventListener(TemplateManager.LOADED_EVENT, loadedHandler);

            // Instantiate the "common" group we defined in common.xml. This ensures
            // the spatial manager and scene are ready and available.
            PBE.templateManager.instantiateGroup("common");

            // Now it's time to start the ElectroServer connection process.
            _es = new ElectroServer();
            // Set the protocol to binary for best performance.
            _es.setProtocol(Protocol.BINARY);
            // Wire up our listeners for connection...
            _es.addEventListener(MessageType.ConnectionEvent, "onConnectionEvent", this);
            // ...login response...
            _es.addEventListener(MessageType.LoginResponse, "onLoginResponse", this);
            // ...game joins...
            _es.addEventListener(MessageType.CreateOrJoinGameResponse, "onCreateOrJoinGameResponse", this);
            // ...and finally, general plugin messages.
            _es.addEventListener(MessageType.PluginMessageEvent, "onPluginMessageEvent", this);
            // Now, create the connection to the server.
            //(This assumes you're running the server locally.)
            _es.createConnection("127.0.0.1", 9899);
        }

        // Handle the connection response from the server.
        public function onConnectionEvent(event:ConnectionEvent):void
        {
            // If the connection is accepted...
            if (event.getAccepted()) {
                // ...generate a user name.
                _name = "user"+Math.round(10000*Math.random());
                // Create and send a login request to the server.
                var loginRequest:LoginRequest = new LoginRequest();
                loginRequest.setUserName(_name);
                _es.send(loginRequest);
            // Otherwise, throw an error.
            } else {
                throw new Error("Connection failed: " + event.getEsError().getDescription());
            }
        }

        // This handles our login response from the server.
        public function onLoginResponse(event:LoginResponse):void
        {
            // Create a new search criteria instance. This is used
            // inside a join game request below.
            var criteria:SearchCriteria = new SearchCriteria();
            // Set the game type to match the name we defined in part one of the tutorial.
            criteria.setGameType("game");

            // Grab the first user variable sent from the server.
            var uservar:UserVariable = event.getUserVariables()[0];
            // Extract the EsObject value from the user variable.
            var esobj:EsObject = uservar.getValue();
            // Make sure we're looking at "gameType".
            if(esobj.doesPropertyExist("gameType"))
            {
                // ElectroServer 4 is not yet capable of adding a player to a game automatically
                // so we'll request it from the client side.
                //
                // Grab the game type.
                var mode:String = esobj.getString("gameType");
                // And create a new join game request.
                var request:QuickJoinGameRequest = new QuickJoinGameRequest();
                // Set the game type we just grabbed.
                request.setGameType(mode);
                // As well as the zone name.
                request.setZoneName(mode + "Zone");
                // Finally, set the criteria and send the request back to the server.
                request.setSearchCriteria(criteria);
                _es.send(request);
            }
        }

        // When a game request response arrives from the server, enter this handler.
        public function onCreateOrJoinGameResponse(event:CreateOrJoinGameResponse):void
        {
            // Check to see if we've successfully joined.
            if(event.getSuccessful() == true)
            {
                // Make sure we have a "type".
                if(event.getGameDetails().doesPropertyExist("type"))
                {
                    // And set that here.
                    _gameType = event.getGameDetails().getString("type");

                    // Store a local reference to the room and zone id.
                    _roomID = event.getRoomId();
                    _zoneID = event.getZoneId();

                    // Tack a listener on to the TemplateManager to notify us when the next file is loaded.
                    PBE.templateManager.addEventListener(TemplateManager.LOADED_EVENT, gameLoadedHandler);
                    // Request that our game.xml file that defines our game is loaded.
                    PBE.templateManager.loadFile("../assets/levels/" + _gameType + ".xml");
                }
            }
            // Otherwise, throw an error.
            else
            {
                throw new Error("Could not join game: " + event.getEsError().getDescription());
            }
        }

        // Jump in this handler once we've loaded our client game assets and are ready to begin.
        private function gameLoadedHandler(event:Event):void
        {
            // Head off to the screen we registered above in the constructor.
            PBE.screenManager.goto(_gameType);

            // Grab the player EsObject that was stored in the _userProps variable. (We'll find
            // out how _userProps got defined below.)
            var player:EsObject = _userProps.getEsObject("player");
            // And get the initial x and y location of the player. We do this now because
            // we finally are able to do something about it; Render it on-screen.
            var spawnPoint:Point = new Point(player.getNumber("x"), player.getNumber("y"));
            // Create a list of opponents that are already in-game.
            var opponentList:Array = _userProps.getEsObjectArray("playerlist");

            // Iterate over each opponent in the list.
            for(var i:uint = 0; i < opponentList.length; i++)
            {
                // Access each opponent EsObject
                var oppObj:EsObject = EsObject(opponentList[i]);
                // Get it's name.
                var name:String = oppObj.getString("name");
                // And if the name exists...
                if(name != _name)
                {
                    // ...extract the current position...
                    var p:Point = new Point(oppObj.getNumber("x"), oppObj.getNumber("y"));
                    // ...and request that a new opponent avatar be spawned.
                    spawnOpponent(name, p);
                }
            }

            // Finally, don't forget to spawn the player's avatar.
            spawnPlayer(spawnPoint);
        }

        // Define what happens when an opponent avatar is created.
        private function spawnOpponent(name:String, spawnPoint:Point):void
        {
            // Based on the name, create a new entity passing in all the parameters
            // it needs to work properly.
            //
            // "opponent" is the name we gave to the template in game.xml. The "@" symbols
            // mean we're accessing a component of the entity template.
            _opponents[name] = PBE.makeEntity("opponent", {
                "@mover.initialPosition":spawnPoint,
                "@mover.translationSpeed":100,
                "@controller.currentRoomID":_roomID,
                "@controller.currentZoneID":_zoneID,
                "@controller.username":name
            });
        }

        // Define what happens when a player avatar is created.
        private function spawnPlayer(spawnPoint:Point):void
        {
            // As mentioned above, create a new entity based on the "player"
            // template we defined in game.xml.
            PBE.makeEntity("player", {
                "@spatial.position":spawnPoint,
                "@controller.connection":_es,
                "@controller.currentRoomID":_roomID,
                "@controller.currentZoneID":_zoneID,
                "@controller.username":_name
            });
        }

        // Define a handler to deal with messages coming in from the plugin.
        public function onPluginMessageEvent(event:PluginMessageEvent):void
        {
            // Get the EsObject from the event object.
            var eso:EsObject = event.getEsObject();
            // Grab the opcode.
            var opcode:int = eso.getInteger("opcode");
            // Set up some shared variables.
            var player:EsObject;
            var name:String = "";

            // switch over the opcode being passed in.
            switch(opcode)
            {
                // When the player was first logged in, we received this message
                // from the server. If you were wondering, this is how _userProps
                // was defined.
                case PLAYER_INIT:
                    _userProps = eso;
                    break;
                // Each time an opponent enters, grab the player object and name and
                // spawn a new opponent avatar.
                case OPPONENT_ENTERED:
                    player = eso.getEsObject("player");
                    name = player.getString("name");
                    if(name != _name)
                        spawnOpponent(name, new Point(player.getNumber("x"), player.getNumber("y")));
                    break;
                // The only state updates we're handling in this demo are position updates.
                // When they arrive, move the corresponding opponent.
                case STATE_UPDATE:
                    name = eso.getString("name");
                    if(name != _name)
                        moveOpponent(name, eso.getNumber("x"), eso.getNumber("y"));
                    break;
                // When a player leaves, request that they be removed from the game.
                case PLAYER_LEFT:
                    name = eso.getString("name");
                    removePlayerFromList(name);
                    break;
                // Die if we don't recognize an opcode.
                default:
                    throw new Error("Unknown opcode: " + opcode);
            }
        }

        // Defines what takes place when the server tells us a player has left.
        private function removePlayerFromList(name:String):void
        {
            // Grab the entity reference to the player...
            var player:IEntity = _opponents[name];
            // ...and destroy it.
            player.destroy();
            // Finally, remove it from the collection.
            delete _opponents[name];
        }

        // Handles moving the opponent avatar on the screen.
        private function moveOpponent(username:String, moveX:Number, moveY:Number):void
        {
            // Grab the entity reference.
            var entity:IEntity = _opponents[username];
            // If it's not null, set a new goal position for the interpolated mover component.
            if(entity != null)
            {
                entity.setProperty(new PropertyReference("@mover.goalPosition"), new Point(moveX, moveY));
            }
        }
    }
}

That’s it! Make sure the server is started and fire up your newly created client. Click inside the swf and remember to use W, A, S and D (or whatever you chose to set inside the KeyboardInput component) and watch your avatar move. Now open up another browser window and point to the same location to see what happens when another client connects. You may want to play a bit with the “nudge” property of the Interpolated2DMoverComponent to get it feeling right but regardless, you’re off and running!

Next Steps

We covered a lot of ground in this tutorial and you may be wondering what the heck to do next. If you struggled a bit with some of the server-side stuff we covered in part one, I’d head over to the ElectroServer forums or wiki and start looking at some of the great tutorials and references they’ve set up there. If you’d like to dive deeper into PushButton Engine, the forums are an excellent place to start learning as well as the docs and awesome IRC channel, #pbengine. Finally, if you’re ready to dig deeper into the world of multiplayer games, I’d urge you to start looking at some pieces that will be integral once you begin creating a production-grade system. Check out articles and tutorials for things like clock/time synchronization, dead reckoning, area of interest management, object state distribution and persistence. If you’re feeling especially brave, check out how we handle custom game protocol management using the Gliese Framework for RedDwarf

If you’ve come this far, thank you. I appreciate your time and your visit. Now go make a great game!


You can download the source code covered in this post here.