SFS2X Docs / AdvancedTopics / mmo-rooms
» MMO Rooms
Since SmartFoxServer 2X version 2.8 we have introduced a new type of Room object, the MMORoom, which supports local interactions between users and game objects based on their proximity.
» Overview
The MMORoom extends the functionality of a regular Room by adding an Area of Interest (AoI in short) to determine the spatial range of the events that will be received by users. The AoI parameter represents the area within which users will affect each other, for example when sending public messages, updating User Variables, etc.
By default the MMORoom does not fire the regular USER_ENTER or USER_EXIT client-side events when users enter or leave the room. Instead the Room's user list is updated via the PROXIMITY_LIST_UPDATE event which provides a delta of the current user list, within the AoI.
In other words the proximity list substitutes the regular user list on the client side, optimizing the number of updates that the user receives. On the server side the full user list of the Room is still accessible in its entirety.
» Intended use
As suggested by the class name, MMORoom objects are useful to create very large, virtually unlimited areas that can contain thousands of players without overloading clients with updates. The MMORoom can be configured to throttle the PROXIMITY_LIST_UPDATE events in order to optimize the network traffic.» Joining and setting a User position
In contrast to regular Rooms, the MMORoom needs to know where each user is located in the 2D or 3D space. The coordinates system is abstract and generic, allowing developers to use any unit of measure (pixels, inches, meters, miles, etc) defined by either 32 bit integers or floating point values.When a user joins the MMORoom his position in the world is still undefined and therefore he will be in a state of limbo until the first SetUserPosition request is sent. In order to avoid users spending too much time in this invisible state each MMORoom can be configured to allow a time out value after which the users will be removed from the Room (NOTE: the time out applies only for users that haven't set their initial position in the room).
As mentioned in the overview, there are no USER_ENTER/EXIT_ROOM events fired to other users as in regular Rooms. The way in which players are updated about other user changes in their proximity (in other words users entering or leaving the AoI) is via the client PROXIMITY_LIST_UPDATE event. All other Room-related events will work like in regular Rooms, including the USER_COUNT_CHANGE which keeps users in the same Room Group updated about the total number of clients in each Room.
Let's take a look at a visual example:
The green area around our player (marked as "ME") represents his AoI: all of the users falling within that area will be able to see our player an exchange messages/events with him. Also, all the players inside the AoI represent the current Room's user list from our client's perspective. In the specific case of this picture we will see just one other player, Piggy, where "see" means receive events related to that player, be aware of his presence in the Room.
Of course during the game the players position will change: each client must keep sending the SetUserPosition request on a regular basis on order to make the system aware of the current positions of all users in the MMORoom and dispatch the PROXIMITY_LIST_UPDATE event accordingly. The rate at which the position should be set can vary a lot, depending on the type of game. Check the tutorials linked at the bottom of the page for a more in-depth discussion on this subject.
Let's suppose that on the next move user Fozzie is entering the AoI and user Piggy is leaving it: the next PROXIMITY_LIST_UPDATE event will reflect this new situation. In particular the event provides two lists containing all the users that have entered the AoI and all those who have left it since the last update. From a client side API perspective this is how the PROXIMITY_LIST_UPDATE event is handled:
private void OnProximityListUpdate(BaseEvent evt) { List<User> added = (List<User>) evt.params["addedUsers"]; List<User> removed = (List<User>) evt.params["removedUsers"]; // Add Users that have entered the proximity list foreach (User user in added) { // Obtain the coordinates at which the User "appeared" in our range Vec3D entryPoint = user.AoiEntryPoint; // Add new avatar in the scene (PSEUDO CODE) AvatarSprite avatarSprite = new AvatarSprite(); avatarSprite.x = entryPoint.px; avatarSprite.y = entryPoint.py; ... } // Remove Users that have left the proximity list foreach (User user in removed) { // Remove the User avatar from the scene... } }
function onProximityListUpdate(evt) { var added = evt.addedUsers; var removed = evt.removedUsers; // Add Users that have entered the proximity list for (var i = 0; i < added.length; i++) { var user = added[i]; // Obtain the coordinates at which the User "appeared" in our range var entryPoint = user.aoiEntryPoint; // Add new avatar on screen (PSEUDO CODE) var avatarSprite = new AvatarSprite(); avatarSprite.x = entryPoint.px; avatarSprite.y = entryPoint.py; ... } // Remove Users that have left the proximity list for (var j = 0; j < removed.length; j++) { // Obtain the User to remove var user = removed[j]; // Remove the User avatar from screen... } }
private function onProximityListUpdate(evt:SFSEvent):void { var added:Array = evt.params.addedUsers; var removed:Array = evt.params.removedUsers; // Add Users that have entered the proximity list for each (var user:User in added) { // Obtain the coordinates at which the User "appeared" in our range var entryPoint:Vec3D = user.aoiEntryPoint; // Add new avatar on screen (PSEUDO CODE) var avatarSprite = new AvatarSprite(); avatarSprite.x = entryPoint.px avatarSprite.y = entryPoint.py ... } // Remove Users that have left the proximity list for (var i:int = 0; i < removed.length; i++) { var userId:int = removed[i]; // Obtain the User to remove var user:User = sfs.userManager.getUserById(userId); // Remove the User avatar from screen... } }
The above snippet is presented as a generic example that should work in any client type (C#, AS3, Java, etc) whether we are using a 2D or a 3D world. The MMORoom coordinate system always works with a 3D system (X, Y, Z) that can be reduced to X and Y for bidimensional applications.
The code shows how to cycle through the addedUsers and removedUsers list in order to take care of the rendering side of things.
» User Variables and Room Variables
UserVariables will work normally in MMORooms, affecting only those Users who are within the AoI of the request sender. RoomVariables, instead, need to be used with parsimony to avoid generating heavy traffic towards the clients. Since RoomVariables contain data that interest all users in the MMORoom, updating them often will generate very large broadcast messages with the risk of saturating the server bandwidth. What is critical here is the rate at which they are updated more than the number of variables used.» Map limits
The MMORoom object accepts a pair of Vec3D parameters representing the limits of the virtual map on the three axis (X, Y, Z). In a 2D and 2.5D application the developer can just use the X and Y coordinates leaving the Z value always set to zero. It is highly recommended to set these limits to restrain the user movement within the range of your virtual world. This way illegal movements can be detected on the server side and denied by the system.
NOTE
Without setting map limits there's a potential risk that malicious users could exhaust the system memory by trying to fill very large number of spaces for extreme coordinate values.
» Obtaining the entry point of other Users
It is usually helpful to know at which coordinates a user has entered the player's AoI. Typically the client code will need to know this position to render a sprite/avatar in the correct spot. By default the MMORoom always sends the entry position of each new User. If this bit of information is not necessary in your application it can be turned off to save extra traffic.
The usage of this feature is demonstrated in the previous code example, by reading the User.aoiEntryPosition value.
It is important to understand that, as the property name says, these are the coordinates of a user when he first entered the AoI of the player, not the current coordinates. In other words the SFS2X API doesn't keep such coordinates in synch with the server automatically. It is responsibility of the developer to find the best strategy to synchronize the users positions on the clients depending on the game type, the client-server lag, etc. Check the tutorials linked at the bottom of the page for a more in-depth discussion on this subject.
» Creating an MMORoom
The MMORoom is created by sending a regular CreateRoomRequest from client side or its equivalent from server-side (see the SFSApi.createRoom method). The only difference is the settings object which must be of class MMORoomSettings. Let's examine the code example below (sfs is the SmartFox class instance):
... // Add room-related event listeners during the SmartFox instance setup sfs.AddEventListener(SFSEvent.ROOM_JOIN, OnRoomJoin); ...
// Create the Room and autojoin it MMORoomSettings cfg = new MMORoomSettings("New MMORoom"); cfg.defaultAOI = new Vec3D(30,25,12); cfg.maxUsers = 5000; cfg.maxSpectators = 0; cfg.mapLimits = new MapLimits(new Vec3D(-3000, -500, -3000), new Vec3D(3000, 500, 3000)); cfg.userMaxLimboSeconds = 20; sfs.Send(new Sfs2X.Requests.CreateRoomRequest(cfg, true));
private void OnRoomJoin(BaseEvent evt) { Room room = (Room)evt.Params["room"]; if (room is MMORoom) { // This is an MMORoom so we need to set the Player position to become visible // In a real case scenario we should find a free position on the virtual map sfs.Send(new Sfs2X.Requests.MMO.SetUserPositionRequest(new Vec3D(100, 100, 100))); // This in turn will trigger a PROXIMITY_LIST_UPDATE with the users present in our Player's AoI } }
... // Add room-related event listeners during the SmartFox instance setup sfs.addEventListener(SFS2X.SFSEvent.ROOM_JOIN, onRoomJoin, this); ...
// Create the Room and autojoin it var cfg = new SFS2X.MMORoomSettings("New MMORoom"); cfg.defaultAOI = new SFS2X.Vec3D(30,25,12); cfg.maxUsers = 5000; cfg.maxSpectators = 0; cfg.mapLimits = new SFS2X.MapLimits(new SFS2X.Vec3D(-3000, -500, -3000), new SFS2X.Vec3D(3000, 500, 3000)); cfg.userMaxLimboSeconds = 20; sfs.send(new SFS2X.CreateRoomRequest(cfg, true));
function onRoomJoin(evt) { if (evt.room instanceof MMORoom) { // This is an MMORoom so we need to set the Player position to become visible // In a real case scenario we should find a free position on the virtual map sfs.send(new SFS2X.SetUserPositionRequest(new SFS2X.Vec3D(100, 100, 100))); // This in turn will trigger a PROXIMITY_LIST_UPDATE with the users present in our Player's AoI } }
... // Add room-related event listeners during the SmartFox instance setup sfs.addEventListener(SFSEvent.ROOM_JOIN, onRoomJoin); ...
// Create the Room and autojoin it var cfg:MMORoomSettings = new MMORoomSettings("New MMORoom"); cfg.defaultAOI = new Vec3D(30,25,12); cfg.maxUsers = 5000; cfg.maxSpectators = 0; cfg.mapLimits = new MapLimits(new Vec3D(-3000, -500, -3000), new Vec3D(3000, 500, 3000)); cfg.userMaxLimboSeconds = 20; sfs.send(new CreateRoomRequest(cfg, true));
private function onRoomJoin(evt:SFSEvent):void { if (evt.params.room is MMORoom) { // This is an MMORoom so we need to set the Player position to become visible // In a real case scenario we should find a free position on the virtual map sfs.send(new SetUserPositionRequest(new Vec3D(100, 100, 100))); // This in turn will trigger a PROXIMITY_LIST_UPDATE with the users present in our Player's AoI } }
First we create the MMORoomSettings and configure some of the MMORoom behaviors. In particular we set the physical limits of virtual world's map and lower the userMaxLimboSeconds property to 20 seconds (default is 50).
We have also set an hypothetical limit of 5000 users for our map. This value can largely vary according to the physical dimensions of the map, those of the characters and entities embodied by players and the size of the explorable areas of such map.
As mentioned earlier when the player is joined in the MMORoom he is still invisible to the other users until a physical position is finally assigned. To do this we handle the ROOM_JOIN event and make sure to immediately send a SetUserPostionRequest.
» Determining the correct entry point on the map
In the last example we have used a simplistic approach to position the player in the virtual map, by using hard coded values. In a real case scenario it is usually required a bit of extra logic to find the right position.
Areas that are non-walkable and spots that are already occupied by other players can't be used to spawn the the player, therefore it is required to apply some game logic before spawning the player.
Depending on how our game works and where the map data is handled we will need to apply such spawning logic on the client or server side. Typically it will be the server side, because from that perspective we can control the whole map and everybody's positions. There can be different approaches on how to choose the spawn location for a player, for instance:
- reusing the previous player's position (e.g. the last location since he/she left the last time);
- a random position within the same area the player was in the last time;
- a random spawn-point within a list of teleport areas available in the map;
- randomly choose any free place;
- etc.
In each case it is highly likely that the data required to calculate the spawn position is located on the server side and therefore it is more convenient to handle this phase in the Extension code. We suggest two possible ways of doing this:
- Join from server side
The client sends a request to the Extension to join an MMORoom, the server executes the join request, calculates the entry point and calls the SFSAPI.setUserPosition(...) method right away.
- Join from client, set position from server
The client sends a regular JoinRoomRequest and handles the relative response. The server Extension is also listening to the ROOM_JOIN event and when it triggers the entry-point logic is executed and the SFSAPI.setUserPosition(...) call is made.
In both cases the client will receive the PROXIMITY_LIST_UPDATE event with the local user list.
» Extensions and user's AoI
From server side code it is also possible to leverage the MMORoom's feature and send custom events that affect only a number of Users within a certain AoI.
In order to show how this works, let's suppose we want to send a custom event to all players that fall within user Piggy's AoI.
MMORoom mmoRoom = (MMORoom) getParentZone().getRoomByName("VirtualMMO"); User piggy = mmoRoom.getUserByName("Piggy"); List<User> recipients = mmoRoom.getProximityList(piggy); // Make sure we have 1 or more recipients if (recipients.size > 0) { ISFSObject message = new SFSObject(); // ...populate the SFSObject with custom data... // Send the object to the selected users send("customMessage", message, recipients); }
It is also possible to provide a custom AoI for more advanced targeting. For example user Piggy could make an action affecting a subset of users which are closer to her, with respect to the total number of users included in the default AoI surrounding her. Make sure to consult the server-side javadoc for further details. All the links are found at the end of this article.
» MMOItems and MMOItem Variables
The MMORoom also provides support for a new entity called MMOItem which represent a non-player entity inside the map. MMOItems can be used as bonuses, triggers, bullets, etc, or any other non-player object that will be handled using the MMORoom's rules of visibility (AoI).
This means that whenever one or more MMOItem falls within the AoI of a Player, it will be notified to the User with a PROXIMITY_LIST_UPDATE event. This is the complete list of parameters provided in the event:
name | type | description |
---|---|---|
room | MMORoom | the target MMORoom |
addedUsers | Array/List | the list of new visible players |
removedUsers | Array/List | the list of removed players |
addedItems | Array/List | the list of new visible MMOItems |
removedItems | Array/List | the list of removed MMOItems |
Each MMOItem is identified by unique ID and, optionally, by a number of custom variables called MMOItem Variables, which behave exaclty like User Variables (the MMOItemVariable class extends UserVariable).
NOTE
There is one important distinction between User Variables and MMOItem Variables: the latter can only be defined and updated from server side.
» Creating an MMOItem from server side
Let's take a look at this simple server side example:
private void createMMOItem() { // Reference to the MMOApi object SFSMMOApi mmoApi = SmartFoxServer.getInstance().getAPIManager().getMMOApi(); // Reference to the game's MMORoom Room targetRoom = getParentZone().getRoomByName("My MMO Room"); // Prepare a list of variables for the MMOItem List<IMMOItemVariables> variables = new LinkedList<IMMOItemVariable>(); variables.add( new MMOItemVariable("type", "bonus") ); variables.add( new MMOItemVariable("points", 250) ); variables.add( new MMOItemVariable("active", true) ); // Create the MMOItem MMOItem mmoItem = new MMOItem(variables); // Deploy the MMOItem in the MMORoom's map mmoApi.setMMOItemPosition(mmoItem, new Vec3D(50, 40, 0), targetRoom); }
In order to deploy a new MMOItem inside the game's Room we need three things:
- a reference to the game MMORoom, which we obtain via the Extension's Zone via the getRoomByName() method;
- a list of variables that will add meaningful properties to the MMOItem itself; in this case we are defining a bonus object with a certain score value and a flag to know if it's active in the world or not;
- the MMOItem object itself which is created in conjuction with the list of variables.
We finally call the setMMOItemPosition(...) to deploy the item which in turn will appear in the next PROXIMITY_LIST_UPDATE on the client inside the addedItems property of the event.
» Managing MMOItems in the game
Depending on the type of game that we are developing and the size of the virtual map we might be generating hundreds of MMOItems to describe bonuses, triggers, bullets and similar entities in the game.
The MMORoom manages these items by keeping track of all MMOItems that were added. If we just need to be able to retrive an item from its unique id we can use the MMORoom.getMMOItemById(...) method otherwise we will be better off by using separate lists for each type of object that we're managing in the game. Example: one list for bullets, one list for triggers, etc.
A common solution to handle several classes of MMOItems in the game is to extend the MMOItem class by adding the local properties (e.g. server side only) that we need:
public class LaserBeamItem extends MMOItem { public static final String TYPE_RED = "red"; public static final String TYPE_YELLOW = "yellow"; public static final String TYPE_GREEN = "green"; private String type; private int strength; public LaserBeamItem(String type) { super(); this.type = type; if (type.equals(TYPE_RED)) this.strength = 100; else if (type.equals(TYPE_YELLOW)) this.strength = 50; else if (type.equals(TYPE_GREEN)) this.strength = 25; } public String getType() { return this.type; } public int getStrength() { return this.strength; } // ... }
This way we can conveniently add our own server side properties and state for each MMOItem while handling the public properties via MMOItemVariables.
If MMOItems are used for bullets or similar objects that have a limited life cycle in the game, we must also make sure to remove them from the Room when they are no longer used. To do this we can call the MMOApi.removeMMOItem(...) method which in turn will update all affected clients via the usual PROXIMITY_LIST_UPDATE.
» Fine tuning the MMORoom performance
A number of optimizations can be achieved by experimenting with the MMORoom settings.
» Update speed
The proximityListUpdateMillis setting in the MMORoomSettings class allows to decide the speed at which the PROXIMITY_LIST_UPDATE events will be fired from the server side. Optimizing this aspect is critical to obtaining proper server and client performance.
The default value is set to 250ms. which may seem somewhat "slow". In reality this setting is already very generous and we may need to increase this value depending on the amount of traffic running in the server.
The reason why PROXIMITY_LIST_UPDATE events are not sent in real time is because in 250ms it's likely that any avatar on screen will have moved very little from his previous position, so little that is unnoticeable. With this in mind we can reduce the amount of events sent to every client by throttling them with the proximityListUpdateMillis parameter.
In the default scenario the server will wait 250ms. before sending the notification that a user has entered or left the player's AoI. If other events of the same type will happen during the short delay they will be aggregated and sent at once, saving traffic. In a very busy MMORoom with hundreds or thousands of players this can make a huge difference in terms of network usage.
Q: Isn't the delay going to slow down the action of the game or MMO world?
No, because the PROXIMITY_LIST_UPDATE has nothing to do with how we manage the physical movements of the players on screen. This part is up to our game logic and the MMORoom won't get in the way. The MMORoom will only signal when users are entering or leaving the player's AoI. A small delay of a few hundred milliseconds will not make any visual difference but it will save significant bandwidth.
Q: What are the recommended proximityListUpdateMillis settings?
For point-and-click type of interaction (MMO virtual worlds, etc) the update interval can be set in the range of 250-1000ms. based on our rendering area size and overall speed of interaction. We recommend to start with the default value and then experiment by raising the value in 100ms increments. The more we can raise the interval without causing apparent lags in the client the best it will be for the bandwidth optimization.
For action/real-time games the default settings (250ms) may be okay and we suggest to apply the previous fine tuning method, only starting from a lower value, e.g. 50ms and gradually increasing to check if it produces any "artifacts" in the game. By artifacts we mean visible lag in the appearance of characters/entities on screen.
» Optimizing update sizes
In MMORooms we will find ourselves using quite a number of User Variables and MMOItem Variables to define custom properties for each player and item on the virtual map.
In order to squeeze the best performance from a bandwidth/network standpoint we suggest to optimize the Variables by using very short names and concise values as much as possible. For example instead of the example we provided in the previous section:
List<IMMOItemVariables> variables = new LinkedList<IMMOItemVariable>(); variables.add(new MMOItemVariable("type", "bonus")); variables.add(new MMOItemVariable("points", 250)); variables.add(new MMOItemVariable("active", true));
we suggest to trim down the variable names as much as possible:
List<IMMOItemVariables> variables = new LinkedList<IMMOItemVariable>(); variables.add(new MMOItemVariable("t", 0)); variables.add(new MMOItemVariable("p", 250)); variables.add(new MMOItemVariable("a", true));
Notice that we also replaced the first variable with a number instead of a string, to further reduce everything down as much as possible. The general rule is that every time we are going to use a string as descriptor for some variable we should consider if it's possible to use a number, which will later be "decoded" to its corresponding value via a key-value table.
An example of this problem is when we need to specify an Avatar type for each User in their Variables. Instead of using a string value such as "Warrior", "Wizard", "Thief", etc, we can use a simple numeric id such as 0, 1, 2... This will save us extra bandwidth especially when thousands of player will be playing together.
» Rendering area vs AoI
Especially in 2D and 2.5D virtual worlds it is important to choose the right size for the AoI based on the size of the viewport on screen. Using an AoI that is slightly larger than the viewport will allow to "hide" the sudden appearance/disappearance of sprites on screen in a very smooth way.
This screenshot illustrate the technique:
The outer dashed red rectangle shows the actual AoI while the inner rectangle is the visible area. By using this strategy avatars will enter and leave the viewport in a seamless way.
To better understand how this works we recommend to check the Simple MMO example tutorial provided under the Flash Examples section.
» Further reading
We have implemented many of the concepts outlined here in a real life example with sources:
- learn from a real exmaple in our Simple MMO example tutorial
- study the advanced MMO API techniques
- also check the server-side javadocs for the com.smartfoxserver.v2.mmo package