Examples (iOS)
Examples (Android)

 

» SpaceWar

» Table of contents

» The game flow

In the next paragraphs we are going to discuss the main game flow, highlighting some portions of the source code for better understanding. The dotted connection lines mean that the steps aren't stricltly linked one to the other.

» Connection, login and Room join

The SpaceWar Zone configuration contains one Room, called Sol, which is the only one available in this demo. As soon as the server is started, the associated Room Extension is initialized; this takes care of instantiating the Game class, which contains the core logic of the simulation making the starships and weapon shots fly in the virtual space.
This class implements the Runnable interface and it's run method is scheduled to be executed every 40 milliseconds (matching the framerate of the client) by means of the TaskScheduler provided by the SmartFoxServer 2X server side API.

// Create main game core
game = new Game(this);

// Schedule task: executes the game logic on the same frame basis (25 fps) used by the Flash client
gameTask = sfs.getTaskScheduler().scheduleAtFixedRate(game, 0, 40, TimeUnit.MILLISECONDS);

After the Sol Room is created and its Extension initialized, also the Zone Extension is initialized: it only takes care of loading and parsing the starships configuration as described previously. More global features could be added to the Zone Extension, like for example the Room/s creation process (instead of relying on the Zone configuration, in order to be able to set more complex custom properties in Room Variables).

Now it's time for the client to connect to the server and perform the login. As mentioned before, a custom login system is active in the Zone. The LoginEventHandler class on the server side listens to the login internal event and adds the starships and weapons configuration to the outgoing parameters that will be delivered to the client in the login confirmation event:

@Override
public void handleServerEvent(ISFSEvent event) throws SFSException 
{ 
    // Retrieve starship models from Zone Extension
    ISFSObject starshipsCfg = ((SpaceWarZoneExtension) this.getParentExtension()).getStarshipsCfg();
    
    // Retrieve weapon models from Zone Extension
    ISFSObject weaponsCfg = ((SpaceWarZoneExtension) this.getParentExtension()).getWeaponsCfg();
    
    // Send starship and weapon models to user as outgoing parameters
    ISFSObject outData = (ISFSObject) event.getParameter(SFSEventParam.LOGIN_OUT_DATA);
    outData.putSFSObject("starships", starshipsCfg);
    outData.putSFSObject("weapons", weaponsCfg);
}

On the client side the same data is retrieved from event's parameters in the Main class:

private function onLogin(evt:SFSEvent):void
{
    // Enable lag monitor
    sfs.enableLagMonitor(true, 1, 5);
    
    // Save username in case it was assigned by the server (guest login for example)
    username = (evt.params.user as User).name;
    
    var data:ISFSObject = evt.params.data;
    
    // Retrieve starship models and weapon models from custom data sent by the Zone Extension
    starshipModels = data.getSFSObject("starships");
    weaponModels = data.getSFSObject("weapons");
    
    // Show starship selection screen
    showStarshipSelScreen();
}

It is important to note that at this stage, on the client, we enable the SFS2X inner lag monitoring system that evaluates the mean value of the client-server-client roundtrip, taking the last five measurements into account. This will be used later when processing the PROXIMITY_LIST_UPDATE and USER_VARIABLES_UPDATE events discussed in the upcoming paragraphs, dividing it by 2 because we are interested in the server-to-client lag only. This is the listener to the event fired by the API when the lag is measured:

private function onPingPong(evt:SFSEvent):void
{
    clientServerLag = (evt.params.lagValue as int) / 2;
}

After the login, the game shows the starships selection screen, which is built dynamically based on the starships configuration sent by the server. In order to be able to dynamically get the right graphics from the spritesheet, the filename of each image used to compose the spritesheet itself must be based on a predefined pattern containing the starship's model as entered in the SpaceWar.cfg external configuration file we discussed previously.

When the starship is selected, the client saves its model in the User Variables, so that the server and the other clients will be able to retrieve it and access its properties during the simulation. The following is the listener in the Main class processing the event fired by the starship selection screen:

private function onStarshipSelect(evt:UIEvent):void
{
    // Save selected starship model to User Variables
    var shipModelUV:UserVariable = new SFSUserVariable(UV_MODEL, evt.data.model);
    sfs.send( new SetUserVariablesRequest([shipModelUV]) );
    
    // Show solar system selection screen
    showSolarSystemSelScreen();
}

The game now proceeds to the solar system selection screen: in other words the selection of the MMORoom to join. Similarly to the starship selection, the screen is built dynamically on the basis of the Room list made available by the API, which in this case contains one item only. The same approach described before is used to display the proper graphics.

When the MMORoom is selected, a join request is sent to the server:

private function onSolarSystemSelect(evt:UIEvent):void
{
    // Get selected solar system
    var roomName:String = evt.data.name;
    
    // Join the corresponding MMORoom
    sfs.send( new JoinRoomRequest(roomName) );
}

As shown in step 11 of the diagram, the client receives the join confirmation event back from the server automatically (no need to handle this in the Extension) so that the main game screen can be displayed. Please note that at this time only the background is displayed: some more steps are necessary before rendering the player and opponents' starships.
The Game class corresponding to the current screen mimics the homonym class on the server side and contains the core logic of the simulation, which makes the starships and weapon shots fly on the stage. This class uses the ENTER_FRAME event: its listener is called on every frame, which means 25 times per second as per our setting. We will discuss the event listener code later.

Other than by the client, the join confirmation event is also handled by the Room Extension on the server side (as shown in the diagram, step 13), by registering the UserJoinRoomEventHandler internal event handler during the Extension initialization. This will be discussed in the next paragraph.

Before moving on, let's give a look at another diagram which shows the detailed client-side flow covering steps from 4 to 12 of the previous image and taking into account all possible error conditions too. The dashed connection lines are not implemented in this example.

Now we can proceed with the player starship initial creation.

» Creating the player starship

The server side event notifying that a user joined the Room is necessary to create a Starship instance in the master simulation. The event listener calls a method on the Room Extension which in turn calls the Game.createStarship method. The second parameter is retrieved from the Zone Extension, which processed the SpaceWar.cfg external configuration file.

public void createStarship(int ownerId, ISFSObject settings)
{
    Starship ship = new Starship(ownerId, settings);
    
    // Set starship random position and rotation angle
    ship.x = Math.random() * 1000;
    ship.y = Math.random() * 800;
    ship.rotation = (int) Math.round(Math.random() * 360 * Math.PI / 180);
    
    ship.lastRenderTime = System.currentTimeMillis();
    
    // Add starship to simulation
    starships.put(ownerId, ship);
    
    // Save initial position and velocity to User Variables and position to the SFS2X Proximity Manager system
    saveStarshipPosition(ship, true);
}

The method sets the initial coordinates and rotation angle of the starship to random values. In particular the position is always inside the client's initial viewport, so that all users join the same area and they can see each other. This is useful for testing purposes.
The method also sets the starship's lastRenderTime property, which will be used later during the actual simulation. The ship "enters" the simulation when it is added to the collection referencing all starships contained in the current MMORoom. In fact the collection is continuously scanned by the scheduled task mentioned in the previous paragraph, to calculate the next position of each starship. We'll discuss this topic in the paragraph describing the starships animation.

It is now time to make the fundamental step of setting the starship position at the SmartFoxServer level too. This is done in two steps and it is a responsibility of the setStarshipState method on the Room Extension. The logic implemented in this method is very important and we'll refer to it again later.

public void setStarshipState(int userId, double x, double y, double vx, double vy, double direction, boolean thrust, int rotation, boolean fireClientEvent)
{
    User user = room.getUserById(userId);
    
    if (user != null)
    {
        // (A) Set User Variables
        List vars = new ArrayList();
        vars.add(new SFSUserVariable(UV_X, x));
        vars.add(new SFSUserVariable(UV_Y, y));
        vars.add(new SFSUserVariable(UV_VX, vx));
        vars.add(new SFSUserVariable(UV_VY, vy));
        vars.add(new SFSUserVariable(UV_DIR, direction));
        vars.add(new SFSUserVariable(UV_THRUST, thrust));
        vars.add(new SFSUserVariable(UV_ROTATE, rotation));
        
        getApi().setUserVariables(user, vars, fireClientEvent, false);
        
        // (B) Set user position in Proximity Manager system
        ...
    }
}

The first half (A) of this method is responsible of saving a number of informations in the User Variables. They include:

At this stage no other user already in the system is aware that the new player just joined the game, because this doesn't happen until his position is set in the Proximity Manager system governing the MMORoom. When setting the User Variables with the fireClientEvent parameter set to true, the only client receiving the USER_VARIABLES_UPDATE event is the player's one. We can take advantage of this behavior to make the client render his own starship on the stage before anything else.

On the client side, in fact, the onUserVarsUpdate listener on the Main class detects that the User Variables for the current player have been set for the first time and calls the Game.createStarship method. Mimicking the behavior of the server side counterpart, this method creates a new Starship instance and adds it to the Dictionary containing all the starships currently rendered by the client (in this case just one). Also the velocity vector is taken into account: in fact the ship could be already moving if, for example, it was created inside a gravity field (not implemented in this demo). From now on the main simulation loop controlled by the ENTER_FRAME event will take care of moving the ship on the stage, as described later on.

Please note that the position, velocity and direction values are of type double, because rounding them would make the client and server simulations diverge on the long run.

» Proximity update

The second half (B) of the setStarshipState method defined in the Room Extension is responsible of setting the user position in the Proximity Manager system for the first time using this simple call:

public void setStarshipState(int userId, double x, double y, double vx, double vy, double direction, boolean thrust, int rotation, boolean fireClientEvent)
{
    User user = room.getUserById(userId);
    
    if (user != null)
    {
        // (A) Set User Variables
        ...
        
        // (B) Set user position in Proximity Manager system
        int intX = (int)Math.round(x);
        int intY = (int)Math.round(y);
        Vec3D pos = new Vec3D(intX, intY, 0);
        mmoApi.setUserPosition(user, pos, this.getParentRoom());
    }
}

Setting the player position triggers (after a maximum of 20 milliseconds, as per the MMORoom configuration we discussed before) the PROXIMITY_LIST_UPDATE event on the client side. This event makes the player aware of all the opponents located within his Area of Interest, and the opponents aware of the new player presence.

Keeping the player's point of view, the handler does the simple job of creating a starship for each opponent listed in the addedUsers list from the event's parameters, retrieving its actual position, velocity, direction, thruster and rotation state from the User Variables. The same with any weapon shot that might be flying around; these are listed in the addedItems array.
Please note that as the actual position coordinates are retrieved from the User Variables, the values set in the Proximity Manager system on the server side can be rounded to integers, because we don't need an extreme precision. In fact the Proximity Manager only governs the mutual visibility of users, and we already configured an oversized AoI.

Back to the client, if we just pay attention to the addedUsers list, we have the following code:

var addedUsers:Array = evt.params.addedUsers;
for each (var au:User in addedUsers)
{
    // Create starship
    getGame().createStarship(au.id, au.name, false, au.getVariable(UV_MODEL).getStringValue());
    
    // Get position-related User Variables
    var x:Number = au.getVariable(UV_X).getDoubleValue();
    var y:Number = au.getVariable(UV_Y).getDoubleValue();
    var vx:Number = au.getVariable(UV_VX).getDoubleValue();
    var vy:Number = au.getVariable(UV_VY).getDoubleValue();
    var d:Number = au.getVariable(UV_DIR).getDoubleValue();
    var t:Boolean = au.getVariable(UV_THRUST).getBoolValue();
    var r:int = au.getVariable(UV_ROTATE).getIntValue();
    
    // Set starship rotating flag
    getGame().setStarshipRotating(au.id, r);
    
    // Set starship position
    getGame().setStarshipPosition(au.id, x, y, vx, vy, d, t, clientServerLag + 10);
}

Looking at step 25 of the diagram, it is important to note that right after each starship is created, the Game.setStarshipPosition method is called (line 20 in the above snippet). This, in turn, calls the Game.renderStarship method responsible of making the starship move at each frame (in fact it is the same method called by the ENTER_FRAME event listener). The reason is that we need to synchronize the positon of the starships (which probably were already moving, as these opponents entered the game before the player) on the client with their position on the server.
By adding the number of milliseconds passed since the server sent the message (server-to-client lag) and half of the proximityListUpdateMillis setting for the MMORoom, the renderStarship method extrapolates the current position of each starship on the server and aligns the client before the simulation loop (the ENTER_FRAME event listener) kicks in. We'll describe this method in detail in a minute.

We now have all the users' starships rendered on the client... it is now time to make them move around.

» Animating the starships

After setting the position/velocity/state of a starship in the User Variables and the position in the Proximity Manager system for the first time after the user joined the game, it is now time for the server simulation to kick in and take care of handling the starships movement.
This is a responsibility of the server side Game.run method, which is executed every 40 milliseconds as already discussed before. In particular this method updates the position of all weapon shots existing in the MMORoom (through method Game.renderWeaponShot) and the position and velocity of all starships (through method Game.renderStarship), also checking the collision against the shots. In particular Game.renderStarship does the following:

private void renderStarship(Starship ship)
{
    long now = System.currentTimeMillis();
    long elapsed = now - ship.lastRenderTime;
    
    for (long i = 0; i < elapsed; i++)
    {
        // Ship rotation
        ship.rotation += ship.rotatingDir * ship.getRotationSpeed();
        
        // Thruster force
        if (ship.thrust)
        {
            ship.velocity.vx += Math.cos(ship.rotation) * ship.getThrustAcceleration();
            ship.velocity.vy += Math.sin(ship.rotation) * ship.getThrustAcceleration();
        }
        
        // Limit speed
        ship.velocity.limitSpeed(ship.getMaxSpeed());
        
        // Update ship position based on its velocity
        ship.x += ship.velocity.vx;
        ship.y += ship.velocity.vy;
    }
    
    ship.lastRenderTime = now;
}

Based on the number of milliseconds passed since the starship was last updated (exactly 40), the method first calculates the new starship rotation angle (if the starship is rotating and in which direction) and the new velocity vector (if the thruster is on); then the starship coordinates are updated accordingly to the velocity.

Just like we did when the starship was created for the first time, we now have to set the new starship state at the SmartFoxServer level and synchronize the clients accordingly: this is done in the last line of the Game.run method by calling Game.saveStarshipPosition, which in turn calls the Room Extension' setStarshipState method we already discussed before. To recap, this method first sets the User Variables and then the position in the Proximity Manager.
In particular, when setting the User Variables we can control if the information should be propagated to the clients or not. Propagating means that the client will syncronize the starship state with the one in the master simulation: then why don't we always pass true as the fireClientEvent parameter, and how to know when to do this or not?

The answer to the first question is simple and actually we already provided it when discussing the clients synchronization: we want to keep the bandwidth usage as low as possible. Suppose we have 10'000 users connected to the same MMORoom and all of them have 100 users in their AoI: we would send a number of user variables update messages (of varying size, depending on the number of variables actually updated) equal to 100x10'000=1'000'000, every 40 milliseconds... it's 25 million messages per second!!! This wouldn't even be a sustainable traffic.

Before providing an answer to the second question instead, you should note that when dealing with the starships movement, we can have two situations: 1) the starship moves along a calculated trajectory resulting from inertia and the thruster status (on/off and thrust direction); 2) the starship suddenly changes its speed and direction due to a weapon hit.
In the first case the client is aware of the starship trajectory, because it knows the current velocity vector of the starships (inertia) and the thruster status: we can avoid sending the User Variables update continuously, letting the client and server simulations proceed in parallel, as they both do the same math. Conversely we should send the update when there's a change in the factors affecting the trajectory. This happens when the user presses or releases the thruster and/or rotation keys (see the "Controlling the starship" paragraph), or in case 2 above, when a weapon shot hits the starship.
This is why the Game.run method sends the User Variables update to the client when a collision is detected only. The Proximity Manager instead is always updated because players must always be informed when an opponent enters/leaves their AoI. In this case the bandwidth usage optimization is part of the inner logic of the MMO API and we don't have to worry about it.

The [calculate position and velocity — update User Variables — update Proximity Manager] iteration is highlighted with a beige box in the flow diagram.

On the client side, the ENTER_FRAME event listener on the Game class governs the simulation. It takes care of updating the positions of all starships and weapon shots in the player's AoI:

private function onFrame():void
{
    // Move weapon shots to next coordinates
    for each (var shot:WeaponShot in weaponShots)
    {
        renderWeaponShot(shot);
    }
    
    // Move starships to next coordinates
    for each (var ship:Starship in starships)
    {
        renderStarship(ship);
    }
}

In particular the Game.renderStarship method mimics its server side counterpart. In addition to the same tasks performed by the server side method, it also updates the starship' sprite position on the stage and takes care of scrolling the starships container when the player reaches the scrolling area limits (see code below).

In case the server logic detects that the User Variables update must be propagated to the client, the Main.onUserVarsUpdate listener receives such update and it calls the Game.setStarshipPosition method. This resets the starship position, velocity, etc, to the values sent by the server and sets the lastRenderTime variable on the basis of the measured server-to-client lag:

public function setStarshipPosition(userId:int, x:Number, y:Number, vx:Number, vy:Number, d:Number, t:Boolean, elapsed:int):void
{
    var ship:Starship = starships[userId];
    
    if (ship != null)
    {
        // Set position and velocity
        ship.xx = x;
        ship.yy = y;
        ship.velocity.vx = vx;
        ship.velocity.vy = vy;
        ship.lastRenderTime = getTimer() - elapsed;
        
        // Set thruster
        ship.doThrust = t;
        
        // Set rotation angle
        ship.rotation = d;
        
        // Render the starship
        // This simulates the starship movement taking into account the elapsed time since the server sent the new position/speed
        // and places the starship in the current coordinates
        renderStarship(ship);
    }
}

The Game.renderStarship method is then called (in addition to the recurring ENTER_FRAME iteration) to align the simulation to the server side one by extrapolating the current position and velocity based on the passed milliseconds (server-to-client lag):

private function renderStarship(ship:Starship):void
{
    var now:Number = getTimer();
    var elapsed:Number = now - ship.lastRenderTime;
    
    for (var i:int = 0; i < elapsed; i++)
    {
        // Ship rotation
        ship.rotation += ship.rotatingDir * ship.rotationSpeed;
        
        // Thruster force
        if (ship.doThrust)
        {
            ship.velocity.vx += Math.cos(ship.rotation) * ship.thrustAcceleration;
            ship.velocity.vy += Math.sin(ship.rotation) * ship.thrustAcceleration;
        }
        
        // Limit speed
        ship.velocity.limitSpeed(ship.maxSpeed);
        
        // Update ship position due to the calculated velocity
        ship.xx += ship.velocity.vx;
        ship.yy += ship.velocity.vy;
    }
    
    // Evaluate background scroll amount
    var scrollX:Number = ship.xx - ship.x;
    var scrollY:Number = ship.yy - ship.y;
    
    // Update starship sprite position in the sprites container
    ship.x = ship.xx;
    ship.y = ship.yy;
    
    ship.lastRenderTime = now;
    
    if (ship.isMine)
    {
        // Scroll sprites container
        var globalCoords:Point = ship.localToGlobal(new Point(0,0));
        var perc:Number = SCROLL_AREA_PADDING / 100;
        
        if (globalCoords.x > (Starling.current.viewPort.width * (1 - perc)))
            container.x -= globalCoords.x - (Starling.current.viewPort.width * (1 - perc));
        if (globalCoords.x < (Starling.current.viewPort.width * perc))
            container.x += (Starling.current.viewPort.width * perc) - globalCoords.x;
        
        if (globalCoords.y > (Starling.current.viewPort.height * (1 - perc)))
            container.y -= globalCoords.y - (Starling.current.viewPort.height * (1 - perc));
        if (globalCoords.y < (Starling.current.viewPort.height * perc))
            container.y += (Starling.current.viewPort.height * perc) - globalCoords.y;
        
        // Scroll background
        bground.scroll(scrollX, scrollY);
    }
}

One last word on the client side PROXIMITY_LIST_UPDATE event. When this event is fired, it means that an opponent' starship (or a weapon shot, but we will discuss it later) entered or left the player's Area of Interest.
We already discussed the case of a ship entering the AoI (see the "Proximity update" paragraph). In case the opponent left the AoI instead, the removedUsers array in the event's parameters simply contains the list of users (and therefore starships) to be removed from the stage and from the Game class inner collection (see the client's Game.removeStarship method).

» Controlling the starship

Players control their starships by activating the thruster. in fact, depending on its rotation angle, by doing so they can modify the starship's inertial trajectory.
Three keyboard keys are involved: left and right arrows for the ship rotation and up arrow for the thrust. Both actions, rotation and thrust, require a similar logic based on the KEY_DOWN and KEY_UP keyboard events and a custom Room Extension request sent by the client to the server. Let's go into details separately.

» Rotation

The KEY_DOWN event for the left and right arrow keys is handled by the client side Game.onKeyboardDown method:

private function onKeyboardDown(evt:KeyboardEvent):void
{
    if (myStarship != null)
    {
        if (evt.keyCode == Keyboard.LEFT || evt.keyCode == Keyboard.RIGHT)
        {
            var dir:int = (evt.keyCode == Keyboard.LEFT ? -1 : +1);
            
            if (dir != myStarship.rotatingDir)
            {
                // Set current rotation direction
                setStarshipRotating(myStarship.userId, dir);
                
                // Fire event to send a request to the server
                dispatchEvent(new ControlEvent(ControlEvent.ROTATE, {rotationDir:myStarship.rotatingDir}) );
            }
        }
    }
}

The dir parameter contains the rotation animation direction (-1 = counterclockwise, 0 = no rotation, +1 = clockwise): the action is processed if the direction changed with respect to the current state only. This is mandatory because Flash keeps firing the keyboard event when the key is down, and without this check we would fire hundred of requests to the server.

The code inside the if statement first sets the rotation parameter on the player starship (in the setStarshipRotating method): this is processed in the Game.renderStarship method discussed before, which updates the rotation angle of the sprite at each frame.
But why do we make the ship rotate immediately, instead of waiting for a command coming from the server (which, remember, is an authoritative server)? The reason is that we can do this immediately because the simple rotation animation doesn't affect the trajectory by itself... and in case the engine is on at the same time (the combined effect of rotation and thrust does modify the trajectory!), a correction will be received from the server soon anyway (read on). On the contrary, waiting for the server before starting the rotation would make the user experience worst, because a lag would be perceived by the player between the key press and the expected result on the screen.

The next action is to send a custom request to the server side Room Extension. We don't do it in the Game class directly, because it is responsibility of the Main class to communicate with the server. So we fire the custom ROTATE event and in its handler on the Main class we send the current rotation direction in the Extension request, to inform the server that the starship started rotating:

private function onStarshipRotate(evt:ControlEvent):void
{
    var params:ISFSObject = new SFSObject();
    params.putInt("dir", evt.data.rotationDir);
    sfs.send( new ExtensionRequest(REQ_ROTATE, params, sfs.lastJoinedRoom) );
}

In other words we don't keep sending the starship angle, but we just let the server know that the rotation started, and in which direction.

The Game.onKeyboardUp method implements the same logic, so there's no need to describe its code. Just remember that when both the left and right keys are released, we simply let the server know that rotation was stopped.

It is now time to move to the server Extension, where the request is handled by the ControlRequestHandler class. This class is common to the "thrust" and "fire" requests too; in this case it calls the dedicated rotateStarship method on the Game class:

public void rotateStarship(int ownerId, int direction)
{
    Starship ship = starships.get(ownerId);
    
    // Set rotating
    ship.rotatingDir = direction;
    
    // On rotation start, set dedicated User Variable
    if (direction != 0)
        ext.setStarshipRotating(ownerId, direction);
    
    // On stop send full position update to clients
    // This includes the current rotation direction (0)
    else
        saveStarshipPosition(ship, true);
}

First of all, this method sets the rotation direction on the proper Starship object, so that it will be later processed in the Game.renderStarship method during the execution of the next scheduled simulation iterations. Then two different actions are executed whether the rotation began or ended.

Starting the rotation

In case the rotation just started (the passed direction is equal to -1 or +1), we need to update all the users in the player's Area of Interest. This is done in the Room Extension's setStarshipRotating method, which just sets a dedicated User Variable:

public void setStarshipRotating(int userId, int direction)
{
    // Set User Variables
    User user = room.getUserById(userId);
    
    if (user != null)
    {
        List<UserVariable> vars = new ArrayList<UserVariable>();
        vars.add(new SFSUserVariable(UV_ROTATE, direction));
        
        getApi().setUserVariables(user, vars, true, false);
    }
}

All clients in the AoI receive the update by means of the onUserVarsUpdate listener we already discussed before. In particular the listener detects a change in the User Variable representing the rotation direction and calls the client side Game.setStarshipRotating method which, again, sets a specific property on the Starship object making it slightly rotate on each subsequent frame.

private function onUserVarsUpdate(evt:SFSEvent):void
{
    var user:User = evt.params.user as User;
    var changedVars:Array = evt.params.changedVars as Array;
    
    if (changedVars.indexOf(UV_ROTATE) > -1)
    {
        // Make user starship start or stop rotating (excluding current user who controls his starship directly)
        if (user != sfs.mySelf)
        {
            var r1:int = user.getVariable(UV_ROTATE).getIntValue();
            getGame().setStarshipRotating(user.id, r1);
        }
    }
    
    ...

Stopping the rotation

Going back to the server side Game.rotateStarship method, if the command coming from the client requests a rotation stop, then the whole starship state (position, velocity, engine state, direction, rotation state) is saved and the corresponding User Variables update is sent to the clients (the true parameter passed to the saveStarshipPosition method). As already discussed before, this causes the starship trajectory on the client to be resynchronized with the server.

» Thrust

The KEY_DOWN event for the up arrow key controlling the thrust is handled by the client side Game.onKeyboardDown method just like for the left/right arrow keys:

private function onKeyboardDown(evt:KeyboardEvent):void
{
    if (myStarship != null)
    {
        if (evt.keyCode == Keyboard.UP)
        {
            if (!isThrustKeyDown)
            {
                isThrustKeyDown = true;
                
                myStarship.thrusterValue = 1;
                
                // Fire event to send a request to the server
                dispatchEvent(new ControlEvent(ControlEvent.THRUST, {activate:true}) );
            }
        }
    }
}

The thrust activation process is made of three steps:

  1. On key down the starship shows a small flame; no actual force is applied to the ship, which means that the trajectory still doesn't change. But this graphical shrewdness gives an immediate feedback to the player, who perceives the lag between the key press and the actual trajectory change as the inertia due to the ship's mass (even in case of a big latency).
  2. A request is sent to the server which activates the thrust and sends a position reset (by means of the usual User Variables update) to all clients, for synchronization purposes. Please note that the code uses the isThrustKeyDown flag to make the client send one thrust activation request to the Room Extension only, even if the listener keeps being called by the Flash Player.
  3. When the event is received the starship shows a bigger flame and the thrust force is applied during the simulation's next iterations (ENTER_FRAME event).

Step 1 above is performed by setting the Starship.thrusterValue property to 1. This makes the starship' sprite display a different frame showing the small thruster flame.

Just like for the rotation request, step 2 is executed by the Main class which listens to the custom THRUST event and sends a request to the Room Extension with a flag indicating that the engine must be activated (the "go" parameter):

private function onStarshipThrust(evt:ControlEvent):void
{
    var params:ISFSObject = new SFSObject();
    params.putBool("go", evt.data.activate);
    sfs.send( new ExtensionRequest(REQ_THRUST, params, sfs.lastJoinedRoom) );
}

On the server side, again the request is processed by the ControlRequestHandler class which calls the dedicated thrustStarship method on the Game class:

public void thrustStarship(int ownerId, boolean activate)
{
    Starship ship = starships.get(ownerId);
    
    // Set rotating
    ship.thrust = activate;
    
    // Set User Variable within the position update event
    saveStarshipPosition(ship, true);
}

This method sets a flag on the proper Starship object; this will cause the thrust force modify the ship's velocity in the Game.renderStarship method during the execution of the next scheduled simulation iterations.
Then the User Variables describing the whole state of the starship are updated, also sending the related event to the clients (the true parameter passed to the saveStarshipPosition method). As already discussed before, this causes the starship trajectory on the clients to be resynchronized with the server.

Step 3 is performed when the User Variables update event is received by the clients: when the starship state is reset to mirror the one on the server, if the thruster is active the Starship.thrusterValue property discussed in step 1 is set to 2.

Thrust deactivation follows the same three steps outlined before, except that the flag sent to the server is false of course. The same approach to reduce the latency perception of the player, based on the graphical feedback, is used here.