SFS2X Docs / ExamplesUnity / spacewar2-p3
» SpaceWar2
» Table of contents
- Setup & run
- Basic concepts
- The game flow
- Controlling the starship
- Firing the weapon
- What next?
- More resources
« Previous | Page 3 of 3 |
» 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 Unity's Input.GetKeyDown() and Input.GetKeyUp() methods and a custom request sent by the client to the Room Extension. All key press and release are checked in the GameEngine.Update() method, as briefly mentioned before. Let's discuss the details.
» Rotation
The GameEngine.OnKeyboardDown() method, called by GameEngine.Update() method, is where we check if the left or right arrow key is pressed.
void OnKeyboardDown() { if (myStarship == null) return; if (Input.GetKeyDown("left") || Input.GetKeyDown("right")) { int dir = Input.GetKeyDown("left") ? 1 : -1; if (dir != myStarship.rotatingDir) { // Start rotation SetStarshipRotating(myStarship.userId, dir); // Fire event to send a request to the server ControlEventArgs eventArgs = new ControlEventArgs(); eventArgs.rotationDir = myStarship.rotatingDir; Rotate?.Invoke(eventArgs); } } ... }
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 the Input.GetKeyDown() check keeps returning true when the key is down, and without this check we would send hundred of requests to the server.
The code inside the inner if statement first sets the rotation parameter on the player starship (in the GameEngine.SetStarshipRotating() method): this is processed in the GameEngine.RenderStarship() method we already discussed before, which updates the rotation angle of the sprite at each update.
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 thrust 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 notification 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 step is to send a custom request to the server-side Room Extension. We don't do it in the GameEngine class directly, because it is responsibility of the main scene controller class to communicate with SmartFoxServer. So we fire the Rotate event of custom ControlShipEvent type and its handler on the controller class sends the current rotation direction in the "control.rotate" Extension request, to inform the server that the starship started rotating.
private void Rotate(ControlEventArgs e) { ISFSObject paramsDir = new SFSObject(); paramsDir.PutInt("dir", e.rotationDir); sfs.Send(new ExtensionRequest(REQ_ROTATE, paramsDir, 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 GameEngine.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 in the released state, we simply let the server know that the starship rotation was stopped.
It is now time to move to the server-side Room Extension, where all "control.*" requests are handled by the ControlRequestHandler class. For the rotation, the dedicated rotateStarship() method on the Game class is called.
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 iteration. Then two different actions are executed whether the rotation began or ended.
Starting the rotation
When the rotation begins (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(Constants.USERVAR_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 GameEngine.SetStarshipRotating() method which, again, sets a specific property on the Starship object making it slightly rotate on each subsequent update.
public void OnUserVarsUpdate(BaseEvent evt) { User user = (User) evt.Params["user"]; ListchangedVars = (List ) evt.Params["changedVars"]; if (changedVars.Contains(UV_ROTATE)) { // Make user starship start or stop rotating (excluding current user who controls his starship directly) if (user != sfs.MySelf) { int r1 = user.GetVariable(UV_ROTATE).GetIntValue(); gameEngine.SetStarshipRotating(user.Id, r1); } } ... }
Stopping the rotation
Going back to the server side's 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, shield) is saved and the corresponding User Variables update is sent to the clients (value true is passed as second parameter to the Game.saveStarshipPosition() method). As already discussed before, this causes the starship trajectory on the client to be resynchronized with the server.
» Thrust
In the GameEngine.OnKeyboardDown() method we also check if the up arrow key is pressed.
void OnKeyboardDown() { ... if (Input.GetKeyDown("up")) { if (!isThrustKeyDown) { isThrustKeyDown = true; myStarship.thrusterValue = 1; // Fire event to send a request to the server ControlEventArgs eventArgs = new ControlEventArgs(); eventArgs.activate = true; Thrust?.Invoke(eventArgs); } } ... }
The thrust activation process is made of three steps:
- 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 starship's mass (even in case of a big latency).
- 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. 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 method keeps being called by Unity.
- When the event is received the starship shows a bigger flame and the thrust force is applied during the simulation's next iterations (Update() method call).
Step 1 above is executed 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 scene controller class which listens to the custom Thrust event and sends the "control.thrust" request to the Room Extension with a flag indicating that the engine must be activated (the "go" parameter).
private void Thrust(ControlEventArgs e) { ISFSObject paramsThrust = new SFSObject(); paramsThrust.PutBool("go", e.activate); sfs.Send(new ExtensionRequest(REQ_THRUST, paramsThrust, sfs.LastJoinedRoom)); }
On the server side, the request is again 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 requester's 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 (value true is passed as second parameter to the Game.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 a graphical feedback, is used here.
» Firing the weapons
Weapons firing is controlled on the client side by a key press (space key for the main weapon, c key for the secondary one) just like the starship rotation and thrust actions. Again, the GameEngine.OnKeyboardDown() method is where we check the key press.
void OnKeyboardDown() { ... if (Input.GetKeyDown("space")) { if (!isFire1KeyDown) { isFire1KeyDown = true; // Fire event to send a request to the server ControlEventArgs eventArgs = new ControlEventArgs(); eventArgs.fire = 1; Fire?.Invoke(eventArgs); } } if (Input.GetKeyDown("c")) { if (!isFire2KeyDown) { isFire2KeyDown = true; // Fire event to send a request to the server ControlEventArgs eventArgs = new ControlEventArgs(); eventArgs.fire = 2; Fire?.Invoke(eventArgs); } } }
First of all, the client can fire 1 shot per key press. For this reason the isFire1KeyDown and isFire2KeyDown flags are set to true and then back to false when the corresponding keys are released (see GameEngine.OnKeyboardUp() method).
Then a custom request is sent to the server-side Room Extension. Just like for the other controls, we are not sending the request directly from the GameEngine class where the key press is detected, but we rather dispatch the custom Fire event. The scene controller class detects the event and sends the "control.fire" Extension request.
private void Fire(ControlEventArgs e) { ISFSObject paramsFire = new SFSObject(); paramsFire.PutInt("wnum", e.fire); sfs.Send(new ExtensionRequest(REQ_FIRE, paramsFire, sfs.LastJoinedRoom)); }
We are using the wnum parameter (containing the value 1 or 2 received from the event dispatcher above) to indicate which weapon has been fired.
On the server side the request is again handled by the ControlRequestHandler class discussed before. The handler's onWeaponFired() method retrieves the weapon settings and calls the Game.fireWeaponShot() method.
public void fireWeaponShot(int ownerId, ISFSObject settings) { Starship ship = starships.get(ownerId); Weapon weapon = ship.getWeapon(settings.getUtfString("model")); // Check if weapon can fire // (at least one shot must be available) if (weapon.isShotAvailable()) { // Increase number of fired shots weapon.increaseShotsCount(); // Create shot instance WeaponShot shot = new WeaponShot(ownerId, settings); // Set shot position equal to starship's position, shifted to its bow or tail int dir = shot.getSpeed() > 0 ? 1 : -1; shot.x = ship.x + 15 * dir * Math.cos(ship.rotation); shot.y = ship.y + 15 * dir * 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 in the SFS2X Proximity Manager system 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. MMOItems are objects representing non-player entities inside an MMORoom. Not to be confused with non-player characters (NPCs, which are treated just like regular users), MMOItems can be used to represent bonuses, triggers, bullets, etc. Such items are subject to the same rules of positioning and visibility of the users in the MMORoom, 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.fireWeaponShot() 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 required by the simulation.
The method then calls the addWeaponShot() member of the Room Extension 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(Constants.ITEMVAR_MODEL, model)); vars.add(new MMOItemVariable(Constants.ITEMVAR_TYPE, Constants.ITEMTYPE_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.fireWeaponShot() 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), it calculates the new position (based exclusively on the shot's velocity and planet's gravity — 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(); // Distance from planet center (always 0,0) double dist = Math.sqrt(Math.pow(shot.x, 2) + Math.pow(shot.y, 2)); boolean isPlanetCrash = planet.radius > 0 && dist < planet.radius; // Check self-destruction or planet crash if (shot.isSelfDestruct() || isPlanetCrash) { // 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() method calls the Room Extension' setWeaponShotPosition() method, 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 and planet or the shot' self destruction. This is a mandatory requirement, to avoid malicious players using modified clients to cheat. But before we discuss the weapon shot 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 in 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, retrieving position and velocity components from MMOItem Variables (also note the usage of the item type variable we discussed before).
public void OnProximityListUpdate(BaseEvent evt) { ... List<IMMOItem> addedItems = (List<IMMOItem>)evt.Params["addedItems"]; foreach (IMMOItem ai in addedItems) { string type = ai.GetVariable(IV_TYPE).GetStringValue(); if (type == ITYPE_WEAPON) { // Get position-related MMOItem Variables string im = ai.GetVariable(IV_MODEL).GetStringValue(); float ix = (float)ai.GetVariable(IV_X).GetDoubleValue(); float iy = (float)ai.GetVariable(IV_Y).GetDoubleValue(); float ivx = (float)ai.GetVariable(IV_VX).GetDoubleValue(); float ivy = (float)ai.GetVariable(IV_VY).GetDoubleValue(); // Create weapon shot gameEngine.CreateWeaponShot(ai.Id, weaponModels.GetSFSObject(im), ix, iy, ivx, ivy, clientServerLag + 10); } } }
From now on the weapon shot is animated when the GameEngine.Update() method is called by Unity. In fact this calls the GameEngine.RenderWeaponShot() method continuously, updating the shot's coordinates based on its velocity and the planet's gravity force until the 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.
As a last step, we need to discuss how weapon 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 their "ageing".
public boolean isSelfDestruct() { if (selfDestructTime > 0) return (System.currentTimeMillis() >= selfDestructTime); return false; }
The selfDestructTime variable is set when the shot is instantiated, adding the "duration" seconds defined in the external configuration to the current timestamp.
Note that weapon shot destruction can be caused by the collision with the planet's surface too, which occurs if its distance from the virtual space center (where the planet is located) is less than the planet's radius (just like for the starships). If it is either time to make the shot self-destruct, or if the collision with the planet occurred, the Game.removeWeaponShot() method is called.
private void removeWeaponShot(WeaponShot shot) { // Check if ship which fired the shot still exists (could have been destroyed already) if (starships.containsKey(shot.ownerId)) { Starship ship = starships.get(shot.ownerId); Weapon weapon = ship.getWeapon(shot.getModel()); // Decrease number of fired shots by owner's ship weapon.decreaseShotsCount(); } // 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 method 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 scene immediately. Of course we also want to show the explosion, not just make the shot disappear. This is the responsibility of the Room Extension's notifyWeaponShotExplosion method:
public void notifyWeaponShotExplosion(int mmoItemId, double x, double y) { // Retrieve list of users which "see" the weapon shot (in other words the shot coordinates are in their AoI) 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(Constants.RESP_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 the "shot_xplode" 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 very 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, in fact, both OnExtensionResponse() and OnProximityListUpdate() listeners on the scene controller class call the GameEngine.RemoveWeaponShot() method.
public void RemoveWeaponShot(int id, bool doExplode, Int32? posX = null, Int32? posY = null) { if (!weaponShots.ContainsKey(id)) return; WeaponShot shot = weaponShots[id]; // The shot could have already been removed if the explosion // was notified by the server before the proximity update if (shot != null) { weaponShots.Remove(id); // Show explosion... if (doExplode) { if (posX != null && posY != null) shot.Explode(ConvertCoords((float)posX, (float)posY)); else shot.Explode(); } // ...or remove immediately else Destroy(shot.gameObject); } }
This removes the weapon shot from the simulation and its sprite from the scene (unless it was already removed, in case the proximity update event was received first) and shows the explosion animation.
» Collisions
Collisions are detected on the server side in the main simulation loop. In fact, while iterating over the starships list to update their positions in the Game.run() method, we also check the distance of each ship from all weapon shots. If the distance from a shot is lower than the "hit radius" configured for the weapon in the external settings file, then the collision occurred.
Note that the weapon shot must also be active: in fact each weapon has a configurable "activation" time expressed in seconds, before which the weapon doesn't explode even if a collision occurs.
@Override public void run() { try { ... for (Iterator<Map.Entry<Integer, Starship>> it = starships.entrySet().iterator(); it.hasNext();) { Starship ship = it.next().getValue(); // Calculate next coordinates renderStarship(ship); // Retrieve list of MMOItems in proximity to check the collision 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); if (shot == null) continue; // Check collision if (shot.isActive() && 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(); // Update starship shield ship.decreaseShield(shot.getDamage()); // Check ship destroyed if (ship.getShield() <= 0) { // Remove ship from simulation starships.remove(ship.getOwnerId()); // Notify starship destruction ext.notifyStarshipExplosion(ship.getOwnerId(), ship.x, ship.y); // Add ship's owner id to game over notification list gameOverIds.add(ship.getOwnerId()); } 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.removeWeaponSho()t 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, in this new version of our SpaceWar example we implemented a number of additional features to make it an actual game, rather than a proof-of-concept showing how to implement client-server synchronization in a realtime game and how to leverage the MMORoom capabilities. Most of those features have been described in this tutorial, while others are up to you to discover in the code. The main ones are:
- A lobby system where multiple game Rooms can be launched.
- The player profile and a match-making system, to find existing games to join based on the player experience.
- The buddy list management, with a private chat in the Lobby scene and the possibility to invite buddies to join an existing game while waiting for it to start.
- A level featuring a planet in its center, with its gravity force added to the math calculating the starships and weapon shots trajectories.
- The starships selection screen showing their characteristics.
- A compass showing where the center of the virtual space is, to make it easier for players to find and attack their opponents; this is represented by a dot rotating around the starship and always pointing towards the center.
- The starship shield, which is reduced every time a weapon shot hits the ship, until it is destroyed and the game is over. The shield status is displayed below the ship' sprite.
- Weapons have a limited number of shots they can fire, which is progressively restored as soon as shots explode in collisions or due to self-destructions.
- A secondary weapon with a different behavior: it is a space mine slower than the primary torpedo, but with a much bigger duration (before self-destruction), hit radius and caused damage.
More features could be implemented to make the game even better. Here 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!
- Solar system. Other than a single planet, the "space" could contain a big star and multiple planets, representing a solar system. The the external config file could be improved to add the coordinates of the planets, their characteristics and so on for each game level.
- 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.
- 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 send 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. Aa always, to avoid cheating, the remaining fuel and its consumption should be calculated on the server side, when the thruster is on.
- Volume of fire. In the current example players can fire all shots of a weapon by pressing and releasing the corresponding key at a fast pace. 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.
- Multiple weapons. Other weapons should be added to the game (an homing missile, a laser beam, etc) to even more diversify the starships, or make it possibile to choose which ones to equip as primary and secondary.
- 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 mentioned in the tutorial).
- 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.
» More resources
You can learn more about the SmartFoxServer concepts discussed in this example by consulting the following resources:
- Game API (MatchExpressions, Invitations)
- The Buddy List API
- MMO Rooms
- Advanced MMO API techniques
- Java Extensions: an overview
- Writing the first Java Extension
- The Java Extension API
« Previous | Page 3 of 3 |