• Examples (iOS)
• Examples (Android)
• Examples (C++)
Server API Documentation

 

» SpaceWar

» Table of contents

» Firing the weapon

Weapon firing is controlled on the client side by a key press (the space, specifically) just like the starship rotation and thrust actions. This is the KEY_DOWN event handler for the space key:

private function onKeyboardDown(evt:KeyboardEvent):void
{
    if (myStarship != null)
    {
        if (evt.keyCode == Keyboard.SPACE)
        {
            if (!isFire1KeyDown)
            {
                isFire1KeyDown = true;
                
                // Fire event to send a request to the server
                dispatchEvent(new ControlEvent(ControlEvent.FIRE, {weapon:1}) );
            }
        }
    }
}

First of all, the client can fire 1 shot per key press. For this reason the isFire1KeyDown flag is set to true and then back to false in the corresponding KEY_UP event handler.

Then a custom request is sent to the server side Room Extension. Just like for the other ship's controls, we are not sending the request directly from the Game class where the keyboard listener is located, but we dispatch the custom FIRE event. The Main class listens to the event and sends the Estension request:

private function onStarshipFire(evt:ControlEvent):void
{
    var params:ISFSObject = new SFSObject();
    params.putInt("wnum", evt.data.weapon);
    sfs.send( new ExtensionRequest(REQ_FIRE, params, sfs.lastJoinedRoom) );
}

We are using the "wnum" parameter (containing the value 1 received from the event dispatcher above) even if this is not strictly needed. In fact in this example all starships can fire a single weapon. Anyway with this parameter the code is already predisposed to add maybe a secondary weapon associated with a different key press.

On the server side the request is handled by the ControlRequestHandler class discussed before. Instead of a method on the Game class directly, this time the handler calls the fireWeapon method on the Room Extension, because we still need to get the weapon settings.

public void fireWeapon(User user, int weaponNum)
{
    // Get starship model selected by user and saved in User Variables
    String shipModel = user.getVariable(UV_MODEL).getStringValue();
    
    // Retrieve weapon settings
    // As the Room Extension and the Zone Extension are loaded by different Class Loaders,
    // the only way to exchange data is using the handleInternalMessage method
    // (fortunately we exchange two SFSObjects between the two, so no class casting issues here)
    ISFSObject data = new SFSObject();
    data.putUtfString("shipModel", shipModel);
    data.putInt("weaponNum", weaponNum);
    
    ISFSObject settings = (ISFSObject) this.getParentZone().getExtension().handleInternalMessage("getWeaponCfg", data);
    
    // Add weapon shot to game
    game.createWeaponShot(user.getId(), settings);
}

Based on the starship model and weapon number, the weapon settings are retrieved from the Zone Extension (note the usage of the handleInternalMessage interface for Room-Zone Extension communication) and now the weapon shot can be created using the Game.createWeaponShot method.

public void createWeaponShot(int ownerId, ISFSObject settings)
{
    WeaponShot shot = new WeaponShot(ownerId, settings);
    
    Starship ship = starships.get(ownerId);
    
    // Set shot position equal to starship's tip position
    shot.x = ship.x + 15 * Math.cos(ship.rotation);
    shot.y = ship.y + 15 * Math.sin(ship.rotation);
    
    // Set shot velocity summing the starship speed and the shot ejection speed
    double vx = Math.cos(ship.rotation) * shot.getSpeed();
    double vy = Math.sin(ship.rotation) * shot.getSpeed();
    Velocity v = new Velocity(vx + ship.getVX(), vy + ship.getVY());
    shot.velocity = v;
    
    shot.lastRenderTime = System.currentTimeMillis();
    
    // Save initial position and velocity to MMOItem Variables and position to the SFS2X Proximity Manager system
    // The ID of the MMOItem in the Proximity Manager system is returned, so we can save it in the shot properties for later reference
    int id = ext.addWeaponShot(shot.getModel(), shot.x, shot.y, shot.getVX(), shot.getVY());
    shot.setMMOItemId(id);
    
    // Add shot to simulation
    weaponShots.put(id, shot);
}

In order to represent the weapon shots in the game we leverage one of the features introduced with the MMO subset of the SmartFoxServer 2X API: MMOItems. As mentioned in the introduction of this tutorial, MMOItems are objects representing non-player entities inside an MMORoom. Their importance is related to the fact that they are subject to the same rules of positioning and visibility of the users in the Room, based on the Area of Interest concept and the Proximity Manager system. In other words when an MMOItem exists in an MMORoom, its presence is notified to the users by means of the same PROXIMITY_LIST_UPDATE event discussed before. Also MMOItems can be moved around, making them the perfect choice to simulate the weapon shots in our game.

The approach taken in this example for the weapon shots management is very similar to what we already explained for the starships representing the players: after creating the object (and in this case we instantiate the MMOItem too, not only the class representing the "bullet" in the simulation) we have to set its position and velocity in the MMOItem Variables (the User Variables counterpart for MMOItems) and the position in the Proximity Manager system.

The Game.createWeaponShot method above creates a WeaponShot instance, calculates its position so that it is placed in front of the starship and evaluates its velocity by summing the ejection speed (set in the external configuration file) and the current speed of the starship. Just like for the starships, the lastRenderTime parameter is used for the simulation.
The method then calls the addWeaponShot member of the Room Extension (responsible of the SFS2X side of the multiplayer logic) which creates the MMOItem associated with the shot. The id of the MMOItem is returned, so we can set it on the WeaponShot instance for later usage.

public int addWeaponShot(String model, double x, double y, double vx, double vy)
{
    // Create MMOItem with its Variables
    List<IMMOItemVariable> vars = buildWeaponShotMMOItemVars(x, y, vx, vy);
    vars.add(new MMOItemVariable(IV_MODEL, model));
    vars.add(new MMOItemVariable(IV_TYPE, ITYPE_WEAPON));
    
    MMOItem item = new MMOItem(vars);
    
    // Set MMOItem position in Proximity Manager system
    setMMOItemPosition(item, x, y);
    
    // Return item ID
    return item.getId();
}

Other than setting the MMOItem position and velocity, the method also saves the weapon model and type in the MMOItem Variables. The model is needed on the client side to retrieve the weapon settings for rendering purpose; the type can be useful in case we need to differentiate the MMOItems (for example we could add bonuses, space debris flying around, etc). In this example this is not strictly needed but we wanted to highlight the possibility.
Finally, when the MMOItem position is set in the Proximity Manager system (done by the setMMOItemPosition method of the Extension), the clients having it in their AoI are notified of its existence through the PROXIMITY_LIST_UPDATE event. We will go back to this in a minute.

So now the weapon shot has been created by the server and added to the list of all shots existing in the virtual space represented by the MMORoom (in the last line of the Game.createWeaponShot server method). It is now responsibility of the usual scheduled task to take care of animating it. In fact the Game.run method iterates over the mentioned list and, unless it is time to make it self-destruct (described later), calculates the new position (based exclusively on the shot's velocity — see the Game.renderWeaponShot method). The new position is then saved in the MMOItem Variables and in the Proximity Manager system.

public void run()
{
    try
    {
        // Move weapon shots to next coordinates
        // Self-destruction is also checked
        for (Iterator<Map.Entry<Integer, WeaponShot>> it = weaponShots.entrySet().iterator(); it.hasNext();)
        {
            WeaponShot shot = it.next().getValue();
            
            // Check self-destruction
            if (shot.isSelfDestruct())
            {
                // Remove shot from simulation
                it.remove();
                
                // Remove shot from Proximity Manager system and notify clients
                removeWeaponShot(shot);
            }
            else
            {
                // Calculate next coordinates
                renderWeaponShot(shot);
                
                // Save position and velocity to User Variables and position to the SFS2X Proximity Manager system
                saveWeaponShotPosition(shot);
            }
        }
        ...

In particular, the Game.saveWeaponShotPosition calls the Room Extension' setWeaponShotPosition which behaves similarly to the setStarshipState method we already discussed:

public void setWeaponShotPosition(int mmoItemId, double x, double y, double vx, double vy)
{
    BaseMMOItem item = room.getMMOItemById(mmoItemId);
    
    // (A) Set MMOItem Variables
    List<IMMOItemVariable> vars = buildWeaponShotMMOItemVars(x, y, vx, vy);
    mmoApi.setMMOItemVariables(item, vars, false);
    
    // (B) Set MMOItem position in Proximity Manager system
    setMMOItemPosition(item, x, y);
}

The subtle difference with respect to the setStarshipState method is that the MMOItem Variables update is never propagated to clients! In fact, as nothing can interfere with the trajectory of a shot, the clients don't need to resynchronize its position and velocity with the server from time to time. We just need to syncronize the initial state when the weapon shot enters the player's AoI, which is done in the PROXIMITY_LIST_UPDATE event listener when the shot is created.

Of course, even if the animation is never synchronized during the time the weapon shot is inside the player's AoI, the server is still in control of the collisions with the starships or the shot' self destruction. This is a mandatory requirement, to avoid malicious players using modified clients to cheat. But before we discuss the shot's destruction, let's take a brief look at the client side.

As mentioned before, the only notification received by a client about weapon shots is the PROXIMITY_LIST_UPDATE event. In particular the addedItems array in the event's parameters contains the list of shots that became "visible". We just need to iterate over the list and create the sprites:

var addedItems:Array = evt.params.addedItems;
for each (var ai:MMOItem in addedItems)
{
    var type:String = ai.getVariable(IV_TYPE).getStringValue();
    
    // Weapon shots
    if (type == ITYPE_WEAPON)
    {
        // Get position-related MMOItem Variables
        var im:String = ai.getVariable(IV_MODEL).getStringValue();
        var ix:Number = ai.getVariable(IV_X).getDoubleValue();
        var iy:Number = ai.getVariable(IV_Y).getDoubleValue();
        var ivx:Number = ai.getVariable(IV_VX).getDoubleValue();
        var ivy:Number = ai.getVariable(IV_VY).getDoubleValue();
        
        // Create weapon shot
        getGame().createWeaponShot(ai.id, im, ix, iy, ivx, ivy, clientServerLag + 10);
    }
}

Position and velocity components are retrieved from MMOItem Variables; please also note the usage of the item type variable we discussed before.
The Game.createWeaponShot method, other than creating the sprite, executes the Game.renderWeaponShot method to align the shot's position with the one on the server (the initial synchronization we already mentioned).

From now on the weapon shot is animated in the usual ENTER_FRAME listener. This calls the Game.renderWeaponShot method recursively, updating the shot's coordinates based on its velocity until a specific destruction notification is received from the server, or the shot leaves the player's AoI. In this case, again, this is notified by the PROXIMITY_LIST_UPDATE event by means of the removedItems array.

The last step we need to take is to understand how shots self-destruction is executed or collision with a starship is detected.

» Self-destruction

To avoid creating MMOItems which possibly move indefinitely inside the MMORoom, we added a self-destruction mechanism for weapon shots. Each weapon has a limited life-span which can be set in the external configuration file we discussed at the beginning of this tutorial.
While the server side Game.run method iterates over the list of shots in the Room to update their positions (as shown before), the WeaponShot.isSelfDestruct method checks its "ageing":

public boolean isSelfDestruct()
{
    if (selfDestructTime > 0)
        return (System.currentTimeMillis() >= selfDestructTime);
    
    return false;
}

where the selfDestructTime variable is set when the shot is instantiated, like this (the "duration" comes from the external configuration):

selfDestructTime = System.currentTimeMillis() + getDuration() * 1000;

If it is time to make the shot self-destruct, the Game.removeWeaponShot method gets called:

private void removeWeaponShot(WeaponShot shot)
{
    // Remove shot from Proximity Manager system
    ext.removeWeaponShot(shot.getMMOItemId());
    
    // Send message to all clients in proximity to remove the shot from the display list
    ext.notifyWeaponShotExplosion(shot.getMMOItemId(), shot.x, shot.y);
}

This calls two more methods on the Room Extension. The first one is straightforward: it removes the MMOItem from the MMORoom. This will cause a PROXIMITY_LIST_UPDATE event to be sent to all users "seeing" the shot, so that they can remove it from the stage immediately. Of course we also want to show the explosion, not just make the shot disappear. This is the responsibility of the next method:

public void notifyWeaponShotExplosion(int mmoItemId, double x, double y)
{
    int intX = (int)Math.round(x);
    int intY = (int)Math.round(y);
    Vec3D pos = new Vec3D(intX, intY, 0);
    
    List<User> users = room.getProximityList(pos);
    
    ISFSObject params = new SFSObject();
    params.putInt("id", mmoItemId);
    params.putInt("x", intX);
    params.putInt("y", intY);
    
    // Send Extension response
    this.send(RES_SHOT_XPLODE, params, users);
}

The method makes use of another feature offered by the MMO API: given the last coordinates of the weapon shot before it was removed, the MMORoom.getProximityList method returns a list of users who have that position inside their Area of Interest. Then a custom Extension response can be sent to all those users to notify them that a specific shot (see the "id" parameter) exploded.

Given that the proximity update is a scheduled task, while the Extension response is sent immediately, it is highly likely that the latter will be delivered to the client before the PROXIMITY_LIST_UPDATE event. Likely, but not 100% guaranteed. For this reason it is important to make sure that the client code is capable of handling the two events independently from their order.

On the client side, the Main.onExtensionResponse listener receives the explosion notification sent by the server (actually in this demo this is the only possible Extension response) and calls the Game.explodeWeaponShot method.

public function explodeWeaponShot(id:int, posX:int, posY:int):void
{
    var shot:WeaponShot = weaponShots[id];
    
    // The shot could have already been removed if the proximity update was notified before the explosion
    if (shot != null)
    {
        // Remove shot
        removeWeaponShot(id);
    }
    
    // Show explosion
    var xplosion:Xplosion = new Xplosion();
    xplosion.x = posX;
    xplosion.y = posY;
    container.addChild(xplosion);
}

This removes the weapon shot from the simulation and the stage by means of the Game.removeWeaponShot method (unless it was already removed, in case the proximity update event was received first) and displays the explosion animation on the stage (which removes itself automatically upon completion).

» Collisions

Collisions are detected on the server side in the main simulation loop. In fact in the Game.run method, while we are iterating over the starships list to update their positions, we also check the distance of each ship from the weapon shots. If the distance is lower than the "hit radius" configured for the weapon in the external settings file, then the collision occurred.

List<Integer> shotIDs = ext.getWeaponShotsList(ship.x, ship.y);

boolean hit = false;

for (int i = 0; i < shotIDs.size(); i++)
{
    int shotID = shotIDs.get(i);
    WeaponShot shot = weaponShots.get(shotID);
    
    // Check collision
    if (getDistance(ship, shot) <= shot.getHitRadius())
    {
        // Remove shot from simulation
        weaponShots.remove(shotID);
        
        // Remove shot from Proximity Manager system and notify clients
        removeWeaponShot(shot);
        
        // Update starship trajectory
        int dirX = (ship.x > shot.x ? +1 : -1);
        int dirY = (ship.y > shot.y ? +1 : -1);
        ship.velocity.vx += dirX * shot.getHitForce();
        ship.velocity.vy += dirY * shot.getHitForce();
        
        hit = true;
    }
}

Of course this process is optimized, in order to avoid to check the collision of all starships (could be thousands) against all shots (tens of thousands). Again the MMO API comes to our rescue, in fact the Room Extension's getWeaponShotsList method makes use of the MMORoom.getProximityItems member, which returns a list of MMOItem objects located in an area corresponding to the configured AoI around the passed coordinates:

public List<Integer> getWeaponShotsList(double x, double y)
{
    List<Integer> shots = new ArrayList<Integer>();
    
    // Get MMOItems in proximity
    int intX = (int)Math.round(x);
    int intY = (int)Math.round(y);
    Vec3D pos = new Vec3D(intX, intY, 0);
    
    List<BaseMMOItem> items = room.getProximityItems(pos);
    
    // Get all MMOItems of type "weapon"
    for (BaseMMOItem item : items)
    {
        boolean isWeapon = item.getVariable(IV_TYPE).getStringValue().equals(ITYPE_WEAPON);
        
        if (isWeapon)
            shots.add(item.getId());
    }
    
    return shots;
}

Back to the Game.run method, when a collision is detected the Game.removeWeaponShot method is called: we already discussed it when describing the self-destruction mechanism, and the flow is exactly the same.

The last step after a collision is detected is to alter the starship's trajectory and notify it to the clients as discussed previously.

» What next?

As stated at the beginning of this tutorial, SpaceWar isn't a full featured game. While the core logic of the space simulation has been fully implemented and largely tested to make it resilient to bad latency conditions, the example lacks a lot of features to make it funny and really enjoyable.
We want to provide here a list of ideas that came to our mind during the development, so maybe you can exapand it and make your own version... the sky is the limit!

» Resources

You can download the diagrams used in this tutorial by clicking on the following links:

» More resources

You can learn more about the described feature by consulting the following resources: