SFS2X Docs / ExamplesUnity / spacewar2-p1
The SpaceWar2 example is a tribute to the homonymous game developed in the 60s, one of the earliest computer games in history! The purpose of this example is to showcase the capabilities of SmartFoxServer's MMORooms in a realtime game featuring flying starships, weapon shots, collisions... with the possibility of having thousands of them in the same Room.
We have a "2" in the game name because this is a new version of the example, in which we implemented a game flow very similar to the one discussed in previous tutorials. After the connection and login steps are executed in the Login scene, a Lobby scene allows interacting with buddies or launch a new game by selecting one of the available levels. After joining the Game Room and switching to the Game scene, the user waits for more players to join and selects a starship among three types with different characteristics (maximum speed, maneuverability, etc). Here the user can also invite buddies to join. When the minimum number of players is reached, a countdown is launched and the actual game starts. All starships are spawned in a region of space and can be controlled using the keyboard: left and right arrow keys to rotate the starship, up arrow key to activate the thruster, space key and "c" key to fire the weapons.
Other than updating the overall game flow, we also added some new features to the game itself with respect to the first version: it is now possible to add a planet with its gravity, starships have a shield and they can be destroyed, they have a secondary weapon, the game has a defined objective and a proper ending.
You should read the comments to methods and properties in the source code for additional informations and possible code optimizations.
» Table of contents
- Setup & run
- Basic concepts
- The game flow
- Controlling the starship
- Firing the weapon
- What next?
- More resources
|Page 1 of 3||Next »|
In order to setup and run the example, follow these steps:
- unzip the examples package;
- under the SmartFoxServer's /SFS2X/extensions folder, create the SpaceWar2 subfolder;
- copy the /Server/SpaceWar2/Extension-Java/deploy folder's content to the server's /SFS2X/extensions/SpaceWar2 folder just created;
- copy the SpaceWar2.zone.xml file (containing the Zone configuration) from the /Server/SpaceWar2/Zone folder to your SFS2X installation folder, under /SFS2X/zones;
- start SmartFoxServer 2X (v2.13 or later is highly recommended);
- launch the Unity Hub, click on the Open button and navigate to the /Client/SpaceWar2 folder;
- if prompted, select the Unity Editor version to use (v2021.3 or later is recommended);
- go to the Project panel, click on the /Assets/Scenes folder and double click on the Login scene to open it;
- click on the Play button to run the example in the Unity Editor, or go to the Build settings panel and build it for your target platform of choice.
The client's C# code is in the /Assets/Scripts folder and the SmartFoxServer 2X client API DLLs are in the /Assets/Plugins folder. Read the introduction to understand why multiple DLLs are used.
» Server-side Extension
The game features two server-side Java Extensions.
In order to access their code, create and setup a new project in your Java IDE of choice as described in the Writing the first Java Extension document of the Java Extensions Development section. Copy the content of the /Server/SpaceWar 2/Extension-Java/src folder to your Java project' source folder.
In SmartFoxServer, games are represented by Rooms: a Room is where users are grouped to be able to interact with each other. As mentioned in the overview, this example leverages one of the main features available in SmartFoxServer 2X: the MMORoom entity. This is a type of Room which has special characteristics aimed at developing Massive Multiplayer Online (MMO) games.
In regular Rooms, whenever a user generates an event (for example sending a public message or setting their own User Variables), this is broadcasted to all users who joined that Room.
In case a Room represents a large virtual space (like in MMO games for example), this can lead to a great waste of resources because usually the user just "sees" a small portion of the whole space: in 2D or 2.5D games this is usually limited by the viewport size and in 3D games by the camera viewing area and the distance from the user's character. So why delivering an event to a user which is not affected at all by that event?
The MMORoom extends the functionality of regular Rooms by adding an Area of Interest (AoI in short) to determine the distance from a user within which events generated by other users are received. In turn, the MMORoom requires that each user declares their own position in the virtual 3D space representing the MMORoom itself. Additionally, as users will likely move inside the virtual space represented by the MMORoom, each client must be notified of other clients entering or leaving their AoI. This is done through the dedicated PROXIMITY_LIST_UPDATE event.
In this example every user is represented by a starship that moves in space. The AoI of each user is an area surrounding the starship and following its movement. The AoI is larger than the viewport: this is done on purpose and the reason will be more clear later. In short, the starship is not centered in the viewport and the PROXIMITY_LIST_UPDATE event is not triggered in realtime (the update rate can be configured by means of one of the properties of the MMORoom). In 2D games in particular, the delay in receiving the update could make a sprite "pop" in or out of the client viewport: the additional area between the viewport and the actual AoI, if carefully configured, can prevent this from happening.
In addition to the concepts just mentioned, this game shows the usage of MMOItems. An MMOItem is an object representing a non-player entity inside an MMO Room. Not to be confused with non-player characters (NPCs, which are treated just like regular users), MMOItems can be used as bonuses, triggers, bullets, etc. Such items are subject to the same rules of visibility of the players in the MMORoom (the AoI in other words) and from a client perspective their nearby existence is notified by the same PROXIMITY_LIST_UPDATE event mentioned before. In this example MMOItems are used to represent the weapon shots moving in space.
Maybe the most complex topic when developing a realtime multiplayer game like this, is to deal with the clients synchronization. Each game type has its own strategies and techniques which include movement prediction, interpolation, latency compensation and what more.
When developing a strategy for synchronization we should always aim at minimizing the amount of data exchanged between the clients and the server. The reason is not to overwhelm the client and the server with messages to be processed, allowing more concurrent users on a single server instance and reducing hosting costs (less server instances, less CPU power per instance, less consumed bandwidth). For this purpose the mechanics of each game must be analyzed in depth to choose the best approach depending on its characteristics.
Our SpaceWar game is a simplified space simulation in which starships and weapons move on an immutable trajectory. This trajectory can be a straight or curved line (depending if planet gravity affects it) which, in case of starships, can be modified by the engine thrust or a collision with a weapon shot: in fact they both alter the ship velocity (speed and direction). Due to the very simple maths involved (essentially we have to sum vectors representing speed), we decided that the best approach was to run the same simulation routine on both the server and the clients on a time basis: we set a framerate of 25 fps in the Unity client and a corresponding scheduled task (running every 1000 / 25 = 40 milliseconds) on the server, and execute the same simulation logic in each frame.
With this approach, the server continuously updates the position of all entities (starships and weapon shots) existing in the MMORoom and the same does each client on its own (of course taking into account the "known" entities only — in other words those falling within the user's Area of Interest). If no alterations to the state of such entities happen, the server and client simulations keep running in parallel without the need to exchange data between them (for example the clients declaring their position to the server continuously). We reduced the data exchanged between the server and the clients to zero (PROXIMITY_LIST_UPDATE event apart).
Of course this is not realistic, because the players interact with the game rotating their starships, turning the thruster on and firing shots: in other words altering the starships trajectories (not to mention possible slowdowns, in particular on the client side, which can make the two simulations diverge even if the player doesn't interact with the game).
In order to keep the simulations in-synch and still exchange a small amount of data, we promoted the server simulation to "master" (in other words we have an authoritative server): all actions causing a trajectory change, for example the player pressing or releasing the thrust key, must be communicated to the server, which updates the entities state accordingly and sends the updates to the clients which in turn reset their local simulations, ensuring an high degree of synchronization.
The lag problem
The client synchronization mechanism described above works perfectly in a local scenario, where no latency exist between the server and the client. But what if we introduce the communication lag? Let's take a look at the following picture:
- The starship is moving along direction d0 at a velocity represented by the blue vector.
- At time t0 the player presses the thrust key after a 90 degrees rotation of the starship, sending a request to the server only (no changes in client simulation).
- At time t1 the server receives the request and processes it, summing the current velocity vector and the thrust vector. Now the resulting velocity direction is d1 and the new vector is sent to the client.
- At time t2 the client receives the update, but in the meanwhile (t2–t1) the starship in the client simulation moved to this new position: setting the new velocity now places the starship on the resulting d2 direction, making it out-of-synch with respect to the server (on the server the starship kept moving along direction d1in fact ).
The t1–t0 latency isn't very important and in any case we can think about some tricks to reduce the lag perceived by the player between the key press and the action being executed (we will discuss this again later in this document).
The t2–t1 lag is instead what causes the client and server simulations to diverge, and we have to take actions to compensate it. The solution adopted in our example is based on measuring the mean lag between the client and the server using the built-in feature of the SmartFox API. Given this value (which approximately indicates the t2–t1 time span), the position the starship had at time t1 and the new velocity vector (both saved in User Variables, as explained later), it is possible to calculate the actual position of the starship on the server side and reset the client simulation to align it with the server one.
The final result is pretty good, even in case of extreme (for a realtime game) latencies of 200 milliseconds or so.
In order to be able to fine tune the behavior of the starship types and weapons available in the game without the need to recompile the server side code each time, we created an external configuration file (SpaceWar2.config) saved in the server-side Extension folder.
This is a text file based on the popular JSON format, which can be easily converted to a SFSOject by means of the SFSObject.newFromJsonData method. This grants immediate access within the game Extension and easy transmission to the clients. The method accepts a string to which we read the configuration file using the org.apache.commons.io.FileUtils class.
The configuration contains:
- A few global settings determining the behavior of the game while players are still joining the Room and they are waiting for the actual game to start; the settings are: the minimum number of players to start the game; a flag indicating if a game can be joined even if already started; the time interval within which players must join before the game is aborted; the countdown seconds before the game is actually started after the minimum number of players is reached.
- The available levels (two in total) for which we can define if a planet is present through its size (radius) and gravity force.
- The available starships (three in total) for which maximum speed, rotation speed, acceleration, shield and weapons can be set.
- the available weapons (two in total) which must be referenced by the starships and whose speed, hit radius, impact force, activation time, duration before self-destruction, number of shots and inflicted damage can be set.
On the server side this example makes use of both a Zone Extension and a Room Extension.
The Zone Extension takes care of loading the global settings and the names of the available levels from the configuration file described above.
The Extension also receives the request to start a game from clients: it looks for an existing Room meeting the requirements and, if one can't be found, creates a new MMORoom joining the requester in it. The Room search-or-create is done automatically by SmartFoxServer thanks to the server-side API's quickJoinOrCreateRoom method we will discuss later.
The Zone Extension is statically assigned to the game Zone in the configuration file deployed on the server (SpaceWar2.zone.xml).
The Room Extension loads the starships (and weapons) configuration described above and transfer it to clients so that users can choose their starship. The Extension also contains the core logic of the game: it updates the game state, processes all events and client requests and runs the master simulation mentioned above and described later in greater detail. Specifically, the SW2RoomExtension class takes care of receiving the requests from clients through a couple of handlers and sends responses to clients to update the game state. The actual simulation logic instead is contained in the Game class. In this way we put in place a separation of duties which makes the code much more friendly to maintain.
The Room Extension is dynamically assigned to every MMORoom when they are created.
As mentioned before, all Rooms created by the Zone Extension are of type MMORoom. They represent a space which contains all player starships, all fired weapon shots and, depending on the level configuration, a planet (the example actually features two levels: one it's just empty space, while the other one contains a planet). The Room is created dynamically and requires a number of common parameters (i.e. the maximum number of players allowed to join, the Room Extension, etc) and some MMORoom-specific settings.
We will discuss the actual Room creation later in this tutorial, but here we want to provide more details on those dedicated settings, as they are related to concepts we already introduced.
In short, the Area of Interest determines the maximum distance at which users see each other, where "see" means that they are notified of each other's presence and can receive/send events from/to each other.
How to determine the size of the AoI? First of all we have to take into account the game characteristics. Considering the standalone build, we want to run the game in a window with resolution (on non-retina displays) of 1440x810 pixels (16:9). Given the default value of 100 pixels per unit for sprites in Unity, in order to achieve a 1:1 pixel ratio we set the orthographic size of the camera in the Game scene to 4.05 units. The black area in the picture below represents our viewport size.
The viewport must follow the user starship, but the camera movement is triggered when the distance of the starship from the viewport border is less than or equal to the 15% of the viewport width/height (the gray rectangle). Doing the math we obtain that the user starship can reach a maximum horizontal distance from the viewport borders of 1440 - 15% = 1224 pixels, and a maximum vertical distance of 810 - 15% = 688 pixels.
Now, 1224x688 is the theoretical minimum value of the AoI, represented by the blue rectangle in the picture below. This is centered on the starship and its size is the double of the AoI value because, for example, the horizontal AoI value is applied to both sides of the starship. A smaller size would often cause the opponents' starships to pop in instead of entering the viewport from outside its limits in a natural way; however using the exact size is still not enough, as the picture below shows.
In fact we have to take latency into account! When the server detects that an opponent (or a weapon shot, represented by an MMOItem) entered the player's Area of Interest and sends them the PROXIMITY_LIST_UPDATE event as a notification, the message takes a variable amount of time to be delivered. During this time period both the player and their opponent keep moving (in the worst case scenario one towards each other). When at last the player receives the event and their client does the math to synchronize its simulation with the actual simulation on the server (as discussed in the "Clients synchronization" paragraph above), it is likely that the opponent will pop in the viewport, as appearing from nowhere.
In order to avoid this behavior (or at least minimize it) we can increase the theoretical AoI's size a little. The actual amount should be a compromise which takes into account the estimated lag, the starships' relative speed in the worst case scenario and the rate at which proximity updates are sent. In our example we decided to use an AoI value of 1300x750 pixels (red rectangle above).
In this example we are not setting physical limits to the MMORoom. Starships can travel indefinitely in any direction. The system is capable of handling this scenario seamlessly, even if usually it is not recommended because malicious users could try to exhaust the system memory by moving their starships to extreme coordinates. Not very likely, but... who knows?
User max limbo seconds
This settings indicates the maximum number of seconds a user is allowed to stay in an MMORoom without setting its initial position (in other words to be in a "limbo" state). If the position, which the proximity system based on the AoI requires, is not set within this time frame, the user is kicked out of the MMORoom.
As the game flow will show more clearly, after an MMORoom is created the game doesn't start immediately, because a minimum number of players must be reached. If such number is not reached within a configurable amount of time (50 seconds), the game is aborted and all players already in the Room are notified and kicked out of it. If the minimum number of players is reached, an additional configurable countdown (10 seconds) is displayed, at the end of which the game actually starts.
Therefore we have to make sure that the time a user is allowed to stay inside the Room before their position is set must exceed the theoretical maximum time required to start the game. This will be set to the sum of the configurable intervals mentioned before (50+10 seconds) plus a few more seconds (20) for tolerance.
Proximity list update rate
The proximity list update milliseconds is a parameter that sets the rate at which the system sends the PROXIMITY_LIST_UPDATE event to clients to notify new users or MMOItems entering/leaving the player's Area of Interest. The lower the value is, the more the system is stressed, especially when thousands of users are inside the same MMORoom.
So, from the point of view of system resources, the best approach would be an higher value. The other side of the coin is that this would increase the overall latency, because the server doesn't send the update immediately: more milliseconds are added to the existing network lag we already discussed.
Let's consider the case in which user A is moving towards user B (who isn't moving) and suddenly A enters B's Area of Interest. If we could measure the added latency, we would just need to compensate it in the client simulation just like we compensate the network lag, and maybe increase the size of the AoI to avoid starship A to pop in user B's viewport (and viceversa).
The problem is that we can't measure it, because we are always in the middle of two extremes: (1) A enters B's AoI an instant before the next proximity update si scheduled; (2) this happens an instant after the previous update was sent. In case of (1) there's no additional latency because the event is fired immediately, so we just have to deal with the network lag; in case of (2) the full value set for the proximityListUpdateMillis parameter is added to the network lag.
Unfortunately we are always stuck somewhere in the middle of the two conditions because we don't know when the update event will be triggered by the scheduler, so we don't know the actual value of added latency to compensate; this, in the end, makes the server and client simulations go out-of-synch. As described later on, the simulation is quite resilient to out-of-synch conditions, because as soon as the trajectory of starship A changes, a new position update is sent (not based on the proximity update event anymore, because A didn't leave B's AoI) and the master and client simulations synchronization is restored. Still this would lead to graphical artifacts, like seeing the starship "jump" back and forth.
To avoid the issue we just described, we set the parameter to a value of 20 milliseconds and, when the proximity update is involved, we always compensate the added latency including half of this value in the math. Of course this will still lead to small errors in the simulation, but the subsequent corrections won't be noticeable.
The last special parameter of an MMORoom is a flag to include the entry points of users (starships) and MMOItems (weapon shots) in the PROXIMITY_LIST_UPDATE event. While the entry points could be needed in other type of games, where you need to know where the opponents first entered the player's AoI, in our example we always reset the client simulation on the basis of the starship's position and velocity vector sent by the server and the time passed since the update message was sent (network lag + half proximityListUpdateMillis value). So we don't actually need this information on the client side, and we can save some bytes by setting the parameter to false.
|Page 1 of 3||Next »|