SFS2X Docs / ExamplesFlash / spacewar-p3
» SpaceWar
» Table of contents
- Installation
- Basic concepts
- The game flow
- Controlling the starship
- Firing the weapon
- What next?
- Resources
- More resources
« Previous | Page 3 of 3 |
» 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 Exstension 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 synchronize 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 expand it and make your own version... the sky is the limit!
- Star and planets. As described in this tutorial, the only MMORoom configured in the game represents a solar system. As such, the "space" should also contain the star and the planets. It would be a nice touch to have an additional configuration section (in the external config file) describing the solar system: the coordinates of the planets, their characteristics and so on.
- Gravity. With the addition of a star and its planets you could also add gravity in the math responsible of calculating the starships and weapon shots trajectories (Game.renderStarship and Game.renderWeaponShot methods). It is a matter of evaluating the distance of the starship/shot from the star or planet and apply an increasing force which alters the velocity components.
- Landing. With the presence of planets, you could make the starships land on the surface similarly to the classic Lander game (controlled speed). This could be used to refuel or restore the energy consumed during the game (shields, weapons), or even teleport on the surface (which could be a separate MMORoom per planet) where to implement different game mechanics.
- Minimap. If a star and planets are added, you might also need a minimap showing the player position in the solar system. A radar on the same minimap could also show the position of the opponents within the player's AoI.
- Other solar systems. The initial solar system selection screen (which in our example features one item only) could display multiple systems to be joined, each corresponding to a different MMORoom. These could be defined in the external configuration file (in order to associate specific properties to each of them) and created at runtime in the Zone Extension instead of statically in the Zone configuration (AdminTool).
- Wormholes. Other than a star and some planets, each solar system could feature one or more wormholes allowing the player to reach other solar systems without leaving the game (to go to the selection screen). After detecting the "collision" with the wormhole, a simple server side Room join call could snd the player to the new solar system.
- Fuel. A starship should deplete its fuel if the engine stays on for too long. It could be restored automatically after some time, or by collecting bonuses or landing on a planet. Always to avoid cheating, the remaining fuel and its consumption should be calculated on the server side, when the thruster is on.
- Shield energy. Similarly to the fuel, a starship should deplete its shield when hit by a weapon shot. If energy goes down to zero, one more hit should cause the ship's destruction and the user being kicked out of the game. The energy could be restored automatically if no shots are taken for a few seconds, or by landing on a planet or collecting bonuses, or even by transferring it from weapons to to shield. Again everything should be handled on the server side.
- Weapon energy. Each time a shot is fired, the weapon energy should be reduced of a unit and then restored when the shot is destroyed, therefore setting a limit to the volume of fire.
- Volume of fire. In the current example players can fire a very large number of shots by pressing and releasing the space key at a fast pace. A malicious client could even cause an infinite number of shots to be fired, because no control has been implemented on the server side. There should be a minimum time between one shot and the next.
- Hyperjump. Just like in the original SpaceWar, a dedicated key could make the starship jump to a near location instantly, as an extreme measure to avoid a shot. This of course should deplete a lot of energy or fuel.
- Secondary weapon. Another weapon in addition to the main one would be a nice touch. It could be a mine, an homing missile (much more complex to achieve) or a laser beam not affected by planets' gravity.
- Asteroids. Other than the planets, the solar system could also contain asteroids flying around. A game mechanic similar to the classic Asteroids game could be added, to make the space flight more difficult because of the debris. Asteroids could even release a bonus when hit. Just like for weapons shots, an asteroid could be represented by an MMOItem in the system (with a different custom "type", as described before).
- Bonuses. Various types of items could float in space to be collected by players: energy cells, fuel recharges, score bonuses, etc. Each time one is collected, a new one should be spawned by the system at a random position. All bonuses could all be represented by different types of MMOItems.
- Starship selection screen. This screen should also display the starships' characteristics returned as custom data during the login process.
» Resources
You can download the diagrams used in this tutorial by clicking on the following links:
- Overall game flow
- Detailed client flow (from connection to game start)
» More resources
You can learn more about the described feature by consulting the following resources:
« Previous | Page 3 of 3 |