Examples (iOS)
Examples (Android)

 

Since 2.8.0

» MMO Demo

mmo-demo

» Overview

The MMO Demo shows how to work with the new objects from the MMO API introduced in SmartFoxServer 2.8.0. The example extends the Object Movement tutorial from this very section, using an MMORoom instead of a regular Room and showing how to work with an Area Of Interest (AoI).

» Prerequisites

In order to follow this tutorial you will need to be familiar with the Object Movement example and also have read the MMO API Overview article. Make sure to check these resources, if necessary:

This simple demo will show how we can handle hundreds or thousands of Users inside a single Room without worrying about overloading the clients with excessive updates. We will also familiarize with how an MMORoom manages the local user list, and learn how to create and configure MMORooms.

>> DOWNLOAD the source files <<

NOTE
In order to test this example with many active clients on our local machines, we provide a simple server Extension that will simulate any number of active Users via the creation of automated NPCs. If you prefer to test with real Users and avoid the NPCs you can set the number of NPCs to zero in the Extension file (see at the end of this document).

» Installation

» Running the example

In order to run the application follow these steps:

  1. make sure your SmartFoxServer 2X installation contains the BasicExamples Zone definition;
  2. copy the /deploy/extension/MMORoomDemo folder to the server's /SFS2X/extensions/ folder;
  3. start SmartFoxServer 2X (v2.8 or later is required, v2.10 or later is highly recommended);
  4. make sure the client runs on the same machine as the server (the IP address can otherwise be changed in the source code);
  5. open the /deploy/MMORoomDemo.html file in a browser.

» Source code setup

The complete project is contained in a zipped folder. To access and build it please follow these steps:

  1. unzip the file contained in the /source folder;
  2. start Unity;
  3. in the Projects panel click on the Open other button and browse to the top folder of the unzipped package, then click Open;
  4. wait for the project setup completion (Unity needs to regenerate some libraries);
  5. go to the Project panel, click on the Assets/Scenes folder and double click on the Game scene to open it.

All relevant assets are contained in the Assets folder; in particular the C# code is in the Scripts subfolder and the SmartFoxServer 2X client API DLLs are contained in the Plugins subfolder. Read the introduction to understand why multiple DLLs are used.

The example features a server-side Extension, as mentioned in the note above. In order to access its code, create and setup a new project in your Java IDE of choice as described in the Writing a basic Extension document. Copy the content of the /source/server/src folder to your project's source folder.

» How it works

The example application allows the user to enter the MMO Room and interact with other players / NPCs, just like in the Object Movement demo. The main difference is that the player will exclusively receive events from Users and entities within his Area of Interest (AoI), thus limiting the amount of communication going on in the Room.

This approach allows to build extremely large maps and game areas that can be mapped to a single server Room, of type MMORoom, and host thousands of players without causing slow downs or excessive network usage. The server will take care of all the details, calculate which client should receive which events according to its position in 3D space, and keep everyone synchronized.

» Code highlights

» Starting up the MMORoom

We will skip the usual connection and login phase because there is nothing new to say: they work exactly as in the previous examples. Instead we will take a look at how the MMORoom is created from client side. Open the ConnectionUI script, attached to UI game object in the Connection scene.

private void OnLogin(BaseEvent evt) {
    string roomName = "UnityMMODemo";

    // We either create the Game Room or join it if it exists already
    if (sfs.RoomManager.ContainsRoom(roomName)) {
        sfs.Send(new JoinRoomRequest(roomName));
    } else {
        MMORoomSettings settings = new MMORoomSettings(roomName);
        settings.DefaultAOI = new Vec3D(25f, 1f, 25f);
        settings.MapLimits = new MapLimits(new Vec3D(-100f, 1f, -100f), new Vec3D(100f, 1f, 100f));
        settings.MaxUsers = 100;
        settings.Extension = new RoomExtension("pyTest", "MMORoomDemo.py");
        sfs.Send(new CreateRoomRequest(settings, true));
    }
}

As soon as we are logged in the Zone we check if the game Room already exists, and if it doesn't, we create a new one, setting the relevant parameters. For this small scale example we set the MMORoomSettings.MaxUsers property to 100, but you are free to experiment with thousands of users provided that you also enlarge the physical map so that more players have room to walk around.

The MMORoomSettings.DefaultAOI parameter is the "crucial" setting that will determine how far around the player events will be detected. Considering that the demo map extends 200 units on the X and Z axis (-100 to 100), we chose to use a size of 25 units for the X and Z axis as the AoI, and 1 unit for the Y axis, since there's no up/down freedom of motion in this example.

Keep in mind that when we specify the size of the AoI for one axis we mean in one direction: in other words 25 units on the X axis mean 25 on the left side and 25 on the right side, for a total of 50 units around the player.

We also define the lowest and highest coordinate available in our map to make sure that we don't exceed the physical size of the 3D world. Finally we attach a simple Java Extension to the MMORoom which will create NPCs to demonstrate the Room's features. If you prefer not to use NPCs you can simply comment out the related line of code.

» Joining the MMORoom

The CreateRoomRequest described before takes a flag as the 2nd parameter to indicate whether or not we want to auto-join the Room and a third parameter to specify which previous Room we might want to leave upon joining.

In our example we do want the auto-join feature, but this is our first and only Room so we pass a null to indicate that there's no Room to leave. The server will join the player and fire a ROOM JOIN event when we're inside. Let's see what the relative event handler does:

private void OnRoomJoin(BaseEvent evt) {
    // Remove SFS2X listeners and re-enable interface before moving to the main game scene
    reset();

    // Go to main game scene
    Application.LoadLevel("Game");
}

Everything is pretty easy. We essentially just load a new Unity scene called "Game" and proceed by removing all event listeners from the current one. This is because we want the new scene to take control of the SmartFox API and respond to the next game events.

It's now time to open to the GameManager script attached to the Game object in the Game scene and take a look at the Start() method.

void Start() {

    if (!SmartFoxConnection.IsInitialized) {
        Application.LoadLevel("Connection");
        return;
    }

    sfs = SmartFoxConnection.Connection;
                
    // Register callback delegates
    sfs.AddEventListener(SFSEvent.CONNECTION_LOST, OnConnectionLost);
    sfs.AddEventListener(SFSEvent.USER_VARIABLES_UPDATE, OnUserVariableUpdate);
    sfs.AddEventListener(SFSEvent.PROXIMITY_LIST_UPDATE, OnProximityListUpdate);
    
    // Get random avatar and color and spawn player
    int numModel = UnityEngine.Random.Range(0, playerModels.Length);
    int numMaterial = UnityEngine.Random.Range(0, playerMaterials.Length);
    SpawnLocalPlayer(numModel, numMaterial);

    // Update settings panel with the selected model and material
    GameUI ui = GameObject.Find("UI").GetComponent("GameUI") as GameUI;
    ui.SetAvatarSelection(numModel);
    ui.SetColorSelection(numMaterial);
}

Here we take a reference to the current SmartFox API object and proceed with registering three event listeners to detect a disconnection, the User Variables update and a new type of event called PROXIMITY LIST UPDATE, which we'll describe in a moment.

Before our avatar can appear in the Room we have to set a number of User Variables which you have already seen in the Object Movement tutorial:

All this is done in the SpawnLocalPlayer() method:

private void SpawnLocalPlayer(int numModel, int numMaterial) {
    Vector3 pos;
    Quaternion rot;
    
    // See if there already exists a model - if so, take its pos+rot before destroying it
    if (localPlayer != null) {
        pos = localPlayer.transform.position;
        rot = localPlayer.transform.rotation;
        Camera.main.transform.parent = null;
        Destroy(localPlayer);
    } else {
        pos = new Vector3(0, 1, 0);
        rot = Quaternion.identity;
    }
    
    // Lets spawn our local player model
    localPlayer = GameObject.Instantiate(playerModels[numModel]) as GameObject;
    localPlayer.transform.position = pos;
    localPlayer.transform.rotation = rot;
    
    // Assign starting material
    localPlayer.GetComponentInChildren<Renderer>().material = playerMaterials[numMaterial];

    // Since this is the local player, lets add a controller and fix the camera
    localPlayer.AddComponent<PlayerController>();
    localPlayerController = localPlayer.GetComponent<PlayerController>();
    localPlayer.GetComponentInChildren<TextMesh>().text = sfs.MySelf.Name;
    Camera.main.transform.parent = localPlayer.transform;
    
    // Lets set the model, material and position and tell the others about it
    // NOTE: we have commented the UserVariable relative to the Y Axis because in this example the Y position is fixed (Y = 1.0)
    // In case your game allows moving on all axis we should transmit all positions
    List<UserVariable> userVariables = new List<UserVariable>();

    userVariables.Add(new SFSUserVariable("x", (double)localPlayer.transform.position.x));
    //userVariables.Add(new SFSUserVariable("y", (double)localPlayer.transform.position.y));
    userVariables.Add(new SFSUserVariable("z", (double)localPlayer.transform.position.z));
    userVariables.Add(new SFSUserVariable("rot", (double)localPlayer.transform.rotation.eulerAngles.y));
    userVariables.Add(new SFSUserVariable("model", numModel));
    userVariables.Add(new SFSUserVariable("mat", numMaterial));

    // Send request
    sfs.Send(new SetUserVariablesRequest(userVariables));
}

The code is pretty straightforward: we instantiate the model for the player, set the material and finally set the text inside the model to the user name of our player.

Next we set all User Variables we mentioned earlier to propagate our avatar settings in the Room. This is where things differ from a regular SmartFoxServer Room until version 2.8. Instead of broadcasting the update to everyone, the system will first check the user position and then, based on the MMORoom's default AoI, will propagate the update only to the players falling within that area.

The trick is accomplished via the server side Extension which is listening for the User Variable update and uses the the X and Z coordinates to tell the MMORoom where the player is located. Here's the relevant server code:

private class UserVariablesHandler extends BaseServerEventHandler
{
	@Override
	public void handleServerEvent(ISFSEvent event) throws SFSException
	{
		@SuppressWarnings("unchecked")
		List variables = (List) event.getParameter(SFSEventParam.VARIABLES);
		User user = (User) event.getParameter(SFSEventParam.USER);
		
		// Make a map of the variables list
		Map varMap = new HashMap();
		for (UserVariable var : variables)
		{
			varMap.put(var.getName(), var);
		}
		
		if (varMap.containsKey("x") && varMap.containsKey("z"))
		{
			Vec3D pos = new Vec3D
			(
				varMap.get("x").getDoubleValue().floatValue(),
				1.0f,
				varMap.get("z").getDoubleValue().floatValue()
			);
			
			mmoAPi.setUserPosition(user, pos, getParentRoom());
		}
	}
}

It should be fairly easy to understand what this snippet does: we put all the variables sent by the client into a map, for convenience, and check if a positional update was sent. If the X or Z position of the player was changed we invoke the MMOApi.setUserPosition()method which is at the core of the MMORoom functioning.

In other words this call tells the MMORoom where the Player is in the map in terms of 2D or 3D coordinates. This in turn allows the MMORoom to keep track of everybody and decide which users should receive which events.

It is important to note that the way in which you keep track of the user position and how the MMORoom keeps track of it are not the same and work independently of one another. From the developer's point of view, we need to keep track of the players' position and we are free to use any system that best suits our game design. Whether it is User Variables or custom values transmitted via Extension it doesn't matter.

On the other hand the MMORoom also needs to be updated about each player position via the MMOApi.setUserPosition(). This is because only by knowing each player's position the server can synchronize clients via their AoI.

The reason why these two tracking systems (the developer's and the server's) are separate is because there is a large variety of games, all requiring different strategies. With this approach you are free to use any game logic that best works for your game and, separately, update the MMORoom state via it's own API method.

PERFORMANCE NOTE
It is not necessary to call the setUserPosition() method on every move of the player. A player moving a few pixels on the left or right won't do any difference to the MMORoom. If you are sending tens of positional updates per second for each player, you may want to reduce the calls tosetUserPosition() to a few per second. You can learn more about this topic in the additional resources found at the end of this article and in the next SpaceWar tutorial.

» The proximity list

The fundamental difference between a regular Room and an MMORoom lies in how the local user list is handled. In the latter, the client does not have a full view of all users joined in the Room; instead it receives a "Proximity User List" which represents a list of all players falling within the Room's AoI range.

The client side events USER ENTER and USER EXIT are no longer available inside an MMORoom; they are substituted by a single event called PROXIMITY LIST UPDATE, which provides a number of parameters:

Whenever a player is entering or leaving the user's AoI an event of this type will be received providing detailed information about what changed.

For the sake of this tutorial, we are only interested in the lists of added and removed users. MMOItems are an interesting advanced feature that is not discussed in this tutorial. You can learn more by consulting the links at the bottom.

Let's now take a look at the OnProximityListUpdate() event handler:

public void OnProximityListUpdate(BaseEvent evt)
{
    var addedUsers = (List) evt.Params["addedUsers"];
    var removedUsers = (List) evt.Params["removedUsers"];
    
    // Handle all new Users
    foreach (User user in addedUsers)
    {
        SpawnRemotePlayer (
            (SFSUser) user, 
            user.GetVariable("model").GetIntValue(), 
            user.GetVariable("mat").GetIntValue(), 
            new Vector3(user.AOIEntryPoint.FloatX, user.AOIEntryPoint.FloatY, user.AOIEntryPoint.FloatZ),
            Quaternion.Euler(0, (float) user.GetVariable("rot").GetDoubleValue() , 0)
        );		
    }
    
    // Handle removed users
    foreach (User user in removedUsers)
    {
        RemoveRemotePlayer((SFSUser) user);
    }
}

When we receive this notification we execute two simple tasks:

When adding new users we need to know at which coordinates we should position the avatar model. This information is provided via a new property added to the User class, called AoiEntryPoint, of type Vec3D.

» Player movement

Similarly to the Object Movement tutorial, we are going to update our player movement via the FixedUpdate() method:

void FixedUpdate() {
    if (sfs != null) {
        sfs.ProcessEvents();
        
        // If we spawned a local player, send position if movement is dirty

        /*
         * NOTE: We have commented the UserVariable relative to the Y Axis because in this example the Y position is fixed (Y = 1.0).
         * In case your game allows moving on all axis you should transmit all positions.
         * 
         * On the server side the UserVariable event is captured and the coordinates are also passed to the MMOApi.SetUserPosition(...) method to update our position in the Room's map.
         * This in turn will keep us in synch with all the other players within our Area of Interest (AoI).
         */
        if (localPlayer != null && localPlayerController != null && localPlayerController.MovementDirty) {
            List userVariables = new List();
            userVariables.Add(new SFSUserVariable("x", (double)localPlayer.transform.position.x));
            //userVariables.Add(new SFSUserVariable("y", (double)localPlayer.transform.position.y));
            userVariables.Add(new SFSUserVariable("z", (double)localPlayer.transform.position.z));
            userVariables.Add(new SFSUserVariable("rot", (double)localPlayer.transform.rotation.eulerAngles.y));
            sfs.Send(new SetUserVariablesRequest(userVariables));
            localPlayerController.MovementDirty = false;
        }
    }
}

On every iteration of this method we are going to check the MovementDirty flag in our localPlayerController object and send a User Variable update with the position and rotation of our Avatar.

NOTE
It is important to point out that this approach is good for local testing but it's not optimized for online playing. This way we will be sending too many updates per second, quickly saturating the internet connection. It is beyond the scope of this example to go in the details of how to optimize the message rate. If you want to learn more please check the other tutorials.

» Other players updates

Last but not least, we want to take a look at how the other players in the MMORoom are animated in the scene. The main event handler driving their motion is the OnUserVariablesUpdate() method:

public void OnUserVariableUpdate(BaseEvent evt) {
    #if UNITY_WSA && !UNITY_EDITOR
    List changedVars = (List)evt.Params["changedVars"];
    #else
    ArrayList changedVars = (ArrayList)evt.Params["changedVars"];
    #endif

    SFSUser user = (SFSUser)evt.Params["user"];
    
    if (user == sfs.MySelf) return;
    
    // Check if the remote user changed his position or rotation
    if (changedVars.Contains("x") || changedVars.Contains("y") || changedVars.Contains("z") || changedVars.Contains("rot")) {
        // Move the character to a new position...
        remotePlayers[user].GetComponent().SetTransform(
            new Vector3((float)user.GetVariable("x").GetDoubleValue(), 1, (float)user.GetVariable("z").GetDoubleValue()),
            Quaternion.Euler(0, (float)user.GetVariable("rot").GetDoubleValue(), 0),
            true
        );
    }

    // Remote client selected new model?
    if (changedVars.Contains("model")) {
        SpawnRemotePlayer(user, user.GetVariable("model").GetIntValue(), user.GetVariable("mat").GetIntValue(), remotePlayers[user].transform.position, remotePlayers[user].transform.rotation);
    }

    // Remote client selected new material?
    if (changedVars.Contains("mat")) {
        remotePlayers[user].GetComponentInChildren().material = playerMaterials[ user.GetVariable("mat").GetIntValue() ];
    }
}

We begin by checking who is the user sending the update. In case it's our own local player we don't need to do anything, otherwise we proceed by checking if any of the three coordinate values changed and update the avatar on screen.

Finally we check if the other avatar properties where modified and proceed to update them as well: the name of the avatar, its material and the model.

» Tweaking the NPCs

A final note on the server side Extension used for this tutorial: you can experiment with the settings of the Extension to generate more or less NPCs in the MMORoom. In order to do so you can change the number of auto-generated NPCs via the MAX_NPC constant defined in the code. If you prefer to remove the NPCs from the demo you can set the value to zero.

» More resources

You can learn more about the SmartFoxServer concepts discussed in this example by consulting the following resources: