Simple MLAPI Authoritative Server

This is my try at a simple authoritative server: where the clients send their commands to the server, the server decides where everyone moves to on the map and sends back the location of all clients to everyone. There is no delta compression when serializing, no client-side prediction or server reconciliation, no lag compensation. In other words: very very simple.

In the picture you can see red capsules (on the Host) that show where the server says everyone is. We do our physics movement on these objects.

The blue capsules are where the server is telling clients everyone is. They have no physics associated with them, they are just moved to the locations received in a message every 1/20 seconds. Notice that the client window has no red capsules: this is because it is not a server and is not calculating where everyone is.

There are six scripts in this project.

CustomTypes
Defines the PlayerCmd type which is what the client sends to the server (mouseButton0, horizontal, vertical, jumpButton)
Defines the PlayerState type which is sent from the server to the clients (List of clientId, position, rotation)
Defines PlayerObjectDictionary which is how a server keeps track of all non-networked red capsules and how clients keep track of non-networked blue capsules
There is code in here that explains to MLAPI how to serialize PlayerCmd and PlayerState over the network

HandlePlayerCmds
The client will save inputs to PlayerCmd every 0.01 sec. After it has saved 5, it sends out an array of PlayerCmds to the server (every 1/20 sec)
Server code is in this file as well. It shows the server saving a client PlayerCmd to a dictionary (uses clientId as a key)

ServerPlayerObjects
This is where the server has a bunch of fake red gameObjects representing clients that it moves around. It is using Unity’s CharacterController to figure out where the objects move. It uses input from each client’s PlayerCmds array.
After 1/20 sec has passed, the code sends out a PlayerState to all clients which is a List of the locations of all clients.

ClientPlayerObjects
This is where the client receives the PlayerState List from the server. It will take each PlayerState and create a fake blue gameObject that represents that client. It simply Lerps each transform to the location reported by the server.

ThirdPersonCamera
This is attached to the fake blue gameObject that ClientPlayerObjects creates that shares our clientId.

NetworkGUI
Displays a menu and bytes sent/received. Note the bytes sent/recv seems to be broken in MLAPI?

The Issue of Networking

The client records user input every 0.01 sec to a PlayerState. Every 0.05 sec it sends this size 5 array to the server. Perfect world: the server takes index 0 and moves the red client object between time 0.0 and 0.01 sec. Between 0.01 and 0.02 it uses index 1, etc. The server sends a record of where every red gameObject was calculated to be to the clients every 0.05 sec. We are using MLAPI and transport Ruffles. We can turn on the Simulator and make it so PlayerState doesn’t get to the server all the time, that messages are delayed, etc. So what we do on the server is (currently) use index 0 between time 0.0 and 0.02 sec (double!), then use index 1 from 0.02 and 0.04, index 2 from 0.04 to 0.06. We still have 2 more PlayerStates we can use. Hopefully by this time a new PlayerState set has arrived from the client. If it hasn’t, we will continue to use the last index 4 to move our red client object. Networking is hard.

Editor Setup

Pretty much the same as the Simple MLAPI Test, with these differences:

Window > MLAPI > Transports > Ruffles > Install Latest
GameObject > NetworkingManager > Select transport... > RufflesTransport
NetworkingManager > Create Player Prefab > Off (We are not using any NetworkTransport)
Ruffles Transport > Log Level > Warning
Ruffles Transport > Simulator > Use Simulator (If you want)
Layer > Add Layer... > Added "Server", "Client", and "Local"
Edit > Project Settings... > Physics > Uncheck Server/Local, etc. (We don't want Host w/ client and server obj to interact)
Important: Attach a Player prefab to ClientPlayerObjects > playerPrefab
Improvements

There’s a lot wrong here. I really struggled with how Unity organizes things vs. a regular C# project with classes. The hardest part of this small project was just trying to organize each script in a logical way! I have a CustomTypes which is used by two other classes but does nothing itself, really. I have a ThirdPersonCamera that needs ClientPlayerObjects to set what it points at. I have HandlePlayerCmds that remembers (on server) what all client input was, and that’s used by ServerPlayerObjects. I have classes exposing “static public” variables for ease of use.

For the networking memory usage, we create a new List every time we send out/receive PlayerState. Create a new array of PlayerCmds when those come in. We constantly send PlayerState even if the player is standing there. This stuff really bothered me.

Source

Found at GitHub
Using MLAPI network library with Ruffles

Simple MLAPI Test

I wanted to try out MLAPI for Unity. Here’s a picture of the Scene I made running a Host (left) and Client (right).

I’ve posted the code to GitHub. Here’s how I made the Scene:

Download MLAPI
Assets > Import Package > Custom Package... > MLAPI-Installer.unitypackage > Import
Window > MLAPI > Install

GameObject > Create Empty > Rename NetworkingManager
Add Component > MLAPI > NetworkingManager > Select transport... > UnetTransport
Add Component > New script > NetworkGUI.cs

GameObject > 3D Object > Capsule > Rename Player
Add Component > Character Controller
Add Component > MLAPI > NetworkedObject
Add Component > MLAPI > NetworkedTransform
Add Component > New script > ThirdPersonController.cs
(Also add a Cube as a Visor to Capsule and set Box Collider off so it doesn't interfere with our camera)
Create > Material > Black > Add to Visor for cool factor

Create prefab of Player, drop in NetworkedPrefabs of NetworkingManager and set Default Player Prefab

GameObject > Create Empty > Rename PlayerStart

Note I couldn’t figure out how to tell how much data was being sent out on the network with MLAPI so I used their NetworkProfiler to estimate bytes per second sent/received.

Split Terrain with Height Interpolation

I was able to fix the blocky steps in the new heightmap from the SplitTerrain code by using the interpolated values in the original terrain instead of the integer values stored in the heightmap. It only took a year to figure this out. 😀

// Height
td.heightmapResolution = heightmapResolution;
float[,] newHeights = new float[heightmapResolution, heightmapResolution];
dimRatio1 = (xMax - xMin) / (heightmapResolution - 1);
dimRatio2 = (zMax - zMin) / (heightmapResolution - 1);
for (int i = 0; i < heightmapResolution; i++)
{
    for (int j = 0; j < heightmapResolution; j++)
    {
        // Divide by size.y because height is stored as percentage
        // Note this is [j, i] and not [i, j] (Why?!)
        newHeights[j, i] = origTerrain.SampleHeight(new Vector3(xMin + (i * dimRatio1), 0, zMin + (j * dimRatio2))) / origTerrain.terrainData.size.y;
    }
}
td.SetHeightsDelayLOD(0, 0, newHeights);

New code is at GitHub.

Rudimentary Stitch Unity Terrain

I’ve added a rudimentary terrain stitch function to the project in the Split Terrain post. It will take the top neighbor and match their bottom edge to our top edge, and it will take our left neighbor and match their right edge to our left one.

void stitchTerrain(GameObject center, GameObject left, GameObject top)
{
    if (center == null)
        return;
    Terrain centerTerrain = center.GetComponent<Terrain>();
    float[,] centerHeights = centerTerrain.terrainData.GetHeights(0, 0, centerTerrain.terrainData.heightmapWidth, centerTerrain.terrainData.heightmapHeight);
    if (top != null)
    {
        Terrain topTerrain = top.GetComponent<Terrain>();
        float[,] topHeights = topTerrain.terrainData.GetHeights(0, 0, topTerrain.terrainData.heightmapWidth, topTerrain.terrainData.heightmapHeight);
        if (topHeights.GetLength(0) != centerHeights.GetLength(0))
        {
            Debug.Log("Terrain sizes must be equal");
            return;
        }
        for (int i = 0; i < centerHeights.GetLength(1); i++)
        {
            centerHeights[centerHeights.GetLength(0) - 1, i] = topHeights[0, i];
        }
    }
    if (left != null)
    {
        Terrain leftTerrain = left.GetComponent<Terrain>();
        float[,] leftHeights = leftTerrain.terrainData.GetHeights(0, 0, leftTerrain.terrainData.heightmapWidth, leftTerrain.terrainData.heightmapHeight);
        if (leftHeights.GetLength(0) != centerHeights.GetLength(0))
        {
            Debug.Log("Terrain sizes must be equal");
            return;
        }
        for (int i = 0; i < centerHeights.GetLength(0); i++)
        {
            centerHeights[i, 0] = leftHeights[i, leftHeights.GetLength(1) - 1];
        }
    }
    centerTerrain.terrainData.SetHeights(0, 0, centerHeights);
}

We run this function on each of our terrain pieces after breaking apart our original terrain.

Split Unity Terrain Script

In writing this script I learned about EditorWindows: using a script to create something outside of the game running. This code will let us launch our script from the newly made menu Split -> Split Terrain. (Code must be placed in Editor directory)

using UnityEngine;
using UnityEditor;

public class SplitTerrain : EditorWindow
{
    [MenuItem("Split/Split Terrain")]
    static void Init()
    {
        GetWindow<SplitTerrain>();
    }

    public void OnGUI()
    {
        // Have our code here
    }
}

When creating the Terrain Manager code, I had wished at the time I knew of a script that would take a large terrain and break it up into smaller parts. So that lead to this post. What follows is a script that takes any size terrain, allows you to enter how many pieces you want it broken into, and what you want the new heightmap, detailmap, and splatmap resolutions to be. To explain that last part, first understand what TerrainData is made of. We have the heightmap which describes the mountains and valleys. The splatmap (or alphamap) that describes what is drawn on the terrain. The detailmap describes where grass is on the terrain, and then there’s an array of tree locations. To break terrain down, we’ll need to take each of these and trim them down.

Now let’s talk about sizes. A terrain’s physical size could be length 200 by width 100. The heightmap is stored in a different array of say size 513. This means that even though the terrain length is twice the size of the width, the heightmap length is 513 and the heightmap width is 513. If we cut the terrain in half lengthwise, we would be at 100 by 100, however the heightmap returned by GetHeights would be 257 by 513 now. We need to either up convert that back to 513 by 513 or go down to 257 by 257, for example. Either way we need to make up data values or lose data points. What the following code does is resize our array to any size the user specifies. heightmapResolution is the new height we are aiming for.

// Get percent of original
float xMinNorm = xMin / origTerrain.terrainData.size.x;
float xMaxNorm = xMax / origTerrain.terrainData.size.x;
float zMinNorm = zMin / origTerrain.terrainData.size.z;
float zMaxNorm = zMax / origTerrain.terrainData.size.z;
float dimRatio1, dimRatio2;

// Height
td.heightmapResolution = heightmapResolution;
float[,] heights = origTerrain.terrainData.GetHeights(
              Mathf.FloorToInt(xMinNorm * origTerrain.terrainData.heightmapWidth),
              Mathf.FloorToInt(zMinNorm * origTerrain.terrainData.heightmapHeight),
              Mathf.FloorToInt((xMaxNorm - xMinNorm) * origTerrain.terrainData.heightmapWidth),
              Mathf.FloorToInt((zMaxNorm - zMinNorm) * origTerrain.terrainData.heightmapHeight));
float[,] newHeights = new float[heightmapResolution, heightmapResolution];
dimRatio1 = (float)heights.GetLength(0) / heightmapResolution;
dimRatio2 = (float)heights.GetLength(1) / heightmapResolution;
for (int i = 0; i < newHeights.GetLength(0); i++)
{
    for (int j = 0; j < newHeights.GetLength(1); j++)
    {
        newHeights[i, j] = heights[Mathf.FloorToInt(i * dimRatio1), Mathf.FloorToInt(j * dimRatio2)];
    }
}
td.SetHeights(0, 0, newHeights);

Here is the result if we upconvert back to 513. This isn’t good. Room for improvment would take and interpolate values. [Update: this has since been fixed]

The trees are stored in an array the size of however many trees you have. Ten trees, size ten. Three thousand trees, size three thousand. The position is stored as a percentage of the size. If we had terrain of size 200 by 100 and one tree was dead center, it’s coord would be stored as 0.5, 0.5. If it was at the edge and to the right some, it would be 0.75, 1. Here is the code that will copy over just the trees in our new piece.

// Tree
for (int i = 0; i < origTerrain.terrainData.treeInstanceCount; i++)
{
    TreeInstance ti = origTerrain.terrainData.treeInstances[i];
    if (ti.position.x < xMinNorm || ti.position.x >= xMaxNorm)
        continue;
    if (ti.position.z < zMinNorm || ti.position.z >= zMaxNorm)
        continue;
    ti.position = new Vector3(((ti.position.x * origTerrain.terrainData.size.x) - xMin) / (xMax - xMin), ti.position.y, ((ti.position.z * origTerrain.terrainData.size.z) - zMin) / (zMax - zMin));
    newTerrain.AddTreeInstance(ti);
}

To explain getting the new position code, if our tree was originally at 0.8 and we were cutting our terrain from 200 to 100 (and keeping the later half), we do 0.8 * 200 which is 160, then subtract the removed part of 100, then divide by 200 minus 100. The new position is 0.6.

You also need to create an asset and save it. Like this.

if (!AssetDatabase.IsValidFolder("Assets/Resources"))
    AssetDatabase.CreateFolder("Assets", "Resources");
// Must do this before Splat
AssetDatabase.CreateAsset(td, "Assets/Resources/" + newName + ".asset");

// Make our terrain

AssetDatabase.SaveAssets();

I save to Resources so I can load it using Resources.LoadAsync in the Terrain Manager code.

If you know of a way to improve this, please let me know. Note the GitHub code is actually at a state where I’ve split some terrain already and you can hit Play and walk around. Here is the link for SplitTerrain.cs.

This code was based on code by Kostiantyn Dvornik. Much thanks to him for sharing!

Simple Multiplayer Terrain Manager for Unity

Here is some code I wrote to load terrain only where the player is. We don’t load the entire map, just the current and neighboring areas.

We will store the terrain data in a dictionary that uses an X and Z coordinate system as a key (Unity uses Z for east-west).

0, 0 0, 1 0, 2
1, 0 1, 1 1, 2
2, 0 2, 1 2, 2

Pictured is the final result with one player at 0, 0 and another at 2, 4. Note this view is from the server where all terrain in use by clients must be loaded. Clients will only load what is needed for their view.

Since we are storing terrain in a dictionary, our TerrainKey will be important to understand. Here is how we assign a TerrainKey:

const int xMax = 3; // How many tiles our north-south is
const int zMax = 6; // How many tiles east-west is
const float terrainSize = 500.0f; // Size of each terrain tile

// Unity uses z instead of y for east-west
public TerrainKey(int x, int z)
{
    this.x = x;
    this.z = z;
}

// Get key from position
public TerrainKey(Vector3 pos)
{
    x = Mathf.FloorToInt(pos.x / terrainSize);
    z = Mathf.FloorToInt(pos.z / terrainSize);
}

// Might be outside range
public bool isValid()
{
    if (x < 0 || x >= xMax)
        return false;
    if (z < 0 || z >= zMax)
        return false;
    return true;
}

public override string ToString()
{
    // Also used when loading the terrain resource
    return string.Format("Terrain{0}_{1}", x, z);
}

Every second we will have our players report their position. We will also want to calculate the neighboring terrain. Note we are marking each grid spot when it is needed by saving a time value. The TerrainManager uses this to know what terrain to load or remove.

// Called by player objects
public static void reportLocation(Vector3 pos)
{
    if (instance == null)
        return;
    TerrainKey key = new TerrainKey(pos);
    if (!key.isValid())
    {
        Debug.Log("Player outside terrain area");
        return;
    }

    // Mark neighbors as being needed
    TerrainKey[] neighbors = key.getNeighbors();
    for (int i = 0; i < neighbors.Length; i++)
    {
        key = neighbors[i];
        instance.terrainDictionary[key].lastNeeded = Time.timeSinceLevelLoad + 1.0f;
    }
}

Here is the function the TerrainManager is calling every second to check if it needs to load or remove terrain. Note we load the terrain resource asynchronously.

// As terrain is flagged as needed or not needed this routine will load or destroy terrain
IEnumerator manageDictionary()
{
    while (true)
    {
        foreach (var pair in terrainDictionary)
        {
            if (pair.Value.lastNeeded <= Time.timeSinceLevelLoad)
            {
                if (pair.Value.gameObject == null)
                    continue;

                // Don't need terrain so remove
                Destroy(pair.Value.gameObject);
            }
            else
            {
                if (pair.Value.gameObject != null)
                    continue;

                // Need terrain so load
                if (!pair.Value.isLoading)
                {
                    pair.Value.isLoading = true;
                    StartCoroutine(loadTerrain(pair));
                }
            }
        }

        // Check every second
        yield return new WaitForSeconds(1.0f);
    }
}

// Loads terrain from resource and sets to correct position
IEnumerator loadTerrain(KeyValuePair<TerrainKey, TerrainValue> pair)
{
    ResourceRequest request = Resources.LoadAsync(pair.Key.ToString());
    yield return null; // Starts again when LoadAsync is done

    TerrainData t = request.asset as TerrainData;
    pair.Value.gameObject = Terrain.CreateTerrainGameObject(t);
    pair.Value.gameObject.transform.position = pair.Key.getPos();
    pair.Value.isLoading = false;
}

When I have time I’d like to use Terrain.SetNeighbors though I don’t know how to measure performance yet.

If you know of a way to improve this, please let me know. Sample code at GitHub. Here is the link for TerrainManager.cs.

Thanks to PandawanFr at Reddit for convincing me to not make things so complicated. Unity’s streaming world demo is here.

Simple Unity MMO Camera Movement Script

I wanted to start messing around with terrain stuff in Unity and I realized I needed a decent camera and movement script, so I wrote one up. I started by looking at the great WOW Camera Movement script.

If you are doing a first person camera, you are looking out from the viewpoint of the player. If it is third person then you are circling around the player and looking down at them. These are some things I wanted to implement:

1) Keyboard up/down moves the player forward/backward and left/right makes the player turn
2) Holding down the right mouse button makes the player strafe left/right instead of turn, and the player steers with the mouse
3) Holding the left mouse button makes the camera look around the player
4) Mouse wheel zooms in and out

Before adding a camera, let’s first work on moving the player. Note there is a lot of discussion about Rigidbody or CharacterController. I am trying CharacterController for now.

I am starting with Unity’s Simple Multiplayer Example because this is what I am familiar with. This is our client side movement code copied from the CharacterController.Move example:

public void FixedUpdate()
{
    if (!isLocalPlayer)
        return;

    var h = Input.GetAxis("Horizontal");
    var v = Input.GetAxis("Vertical");

    // Only allow user control when on ground
    if (controller.isGrounded)
    {
        moveDirection = new Vector3(h, 0, v); // Strafe
        moveDirection = transform.TransformDirection(moveDirection);
        moveDirection *= 6.0f;
        if (Input.GetButton("Jump"))
            moveDirection.y = 8.0f;
    }

    moveDirection.y -= 20.0f * Time.deltaTime; // Apply gravity
    controller.Move(moveDirection * Time.deltaTime);
}

The player can move forwards/backwards and strafe left/right. Now lets make it so that only if the right mouse button is held down we strafe, otherwise we turn. This is copying transform.Rotate from CharacterController.SimpleMove.

public void FixedUpdate()
{
    if (!isLocalPlayer)
        return;

    var h = Input.GetAxis("Horizontal");
    var v = Input.GetAxis("Vertical");

    if (!Input.GetMouseButton(1)) // NEW
        transform.Rotate(0, h * 3.0f, 0); // Turn left/right

    // Only allow user control when on ground
    if (controller.isGrounded)
    {
        if (Input.GetMouseButton(1)) // NEW
            moveDirection = new Vector3(h, 0, v); // Strafe
        else
            moveDirection = Vector3.forward * v; // Move forward/backward

        moveDirection = transform.TransformDirection(moveDirection);
        moveDirection *= 6.0f;
        if (Input.GetButton("Jump"))
            moveDirection.y = 8.0f;
    }

    moveDirection.y -= 20.0f * Time.deltaTime; // Apply gravity
    controller.Move(moveDirection * Time.deltaTime);
}

Now let’s attach a camera that follows the player:

public void LateUpdate()
{
    if (!isLocalPlayer)
        return;

    float cameraPitch = 40.0f;
    float cameraYaw = 0;
    float cameraDistance = 5.0f;
    Transform cameraTarget = transform; // Camera will always face this

    cameraYaw = cameraTarget.eulerAngles.y;

    // Calculate camera position
    Vector3 newCameraPosition = cameraTarget.position + (Quaternion.Euler(cameraPitch, cameraYaw, 0) * Vector3.back * cameraDistance);

    Camera.main.transform.position = newCameraPosition;
    Camera.main.transform.LookAt(cameraTarget.position);
}

To make it so we can use the mouse to look around we move some variables outside the function. We also want to make sure we don’t place the camera inside anything so we use Physics.Linecast.

public void LateUpdate()
{
    if (!isLocalPlayer)
        return;

    // NEW
    // If mouse button down then allow user to look around
    if (Input.GetMouseButton(0) || Input.GetMouseButton(1))
    {
        cameraPitch += Input.GetAxis("Mouse Y") * 2.0f;
        cameraPitch = Mathf.Clamp(cameraPitch, -10.0f, 80.0f);
        cameraYaw += Input.GetAxis("Mouse X") * 5.0f;
        cameraYaw = cameraYaw % 360.0f;
    }
    else
    {
        cameraYaw = cameraTarget.eulerAngles.y;
    }

    // NEW
    // Zoom
    if (Input.GetAxis("Mouse ScrollWheel") != 0)
    {
        cameraDistance -= Input.GetAxis("Mouse ScrollWheel") * 5.0f;
        cameraDistance = Mathf.Clamp(cameraDistance, 2.0f, 12.0f);
    }

    // Calculate camera position
    Vector3 newCameraPosition = cameraTarget.position + (Quaternion.Euler(cameraPitch, cameraYaw, 0) * Vector3.back * cameraDistance);

    // NEW
    // Does new position put us inside anything?
    RaycastHit hitInfo;
    if (Physics.Linecast(cameraTarget.position, newCameraPosition, out hitInfo))
    {
        newCameraPosition = hitInfo.point;
    }

    Camera.main.transform.position = newCameraPosition;
    Camera.main.transform.LookAt(cameraTarget.position);
}

In the movement code we want to make it so the character faces where the camera is pointed if mouse button two is down.

if (Input.GetMouseButton(1))
    transform.rotation = Quaternion.Euler(0, cameraYaw, 0); // Face camera
else
    transform.Rotate(0, h * 3.0f, 0); // Turn left/right

There are two problems with the camera “snapping” quickly so we add lerping.

1) When your camera is blocked by an object behind you and the player, and when it moves away you need to slowly move back to the far away zoom (fixed with my variable lerpDistance)
2) When you are moving via the keyboard and you release the left mouse button then the camera needs to slowly move back behind the player (fixed with my variable lerpYaw)

We flag lerpDistance off if zooming because we want this to be fast, but if we are hitting something behind us we flag to lerp the camera distance when it’s gone. Note this works because the Camera.main.transform.position is changing each time. Every time we use lerp we need something to be updating/different from last call.

// Does new position put us inside anything?
RaycastHit hitInfo;
if (Physics.Linecast(cameraTarget.position, newCameraPosition, out hitInfo))
{
    newCameraPosition = hitInfo.point;
    lerpDistance = true;
}
else
{
    // NEW
    if (lerpDistance)
    {
        float newCameraDistance = Mathf.Lerp(Vector3.Distance(cameraTarget.position, Camera.main.transform.position), cameraDistance, 5.0f * Time.deltaTime);
        newCameraPosition = cameraTarget.position + (Quaternion.Euler(cameraPitch, cameraYaw, 0) * Vector3.back * newCameraDistance);
    }
}

For when moving, we remember if the client wants to move by adding lerpYaw to the movement code:

var h = Input.GetAxis("Horizontal");
var v = Input.GetAxis("Vertical");

// Have camera follow if moving
if (!lerpYaw && (h != 0 || v != 0))
    lerpYaw = true;

// This is in LateUpdate()
if (lerpYaw)
    cameraYaw = Mathf.LerpAngle(cameraYaw, cameraTarget.eulerAngles.y, 5.0f * Time.deltaTime);

That should be it. The final result is three clients chilling.

If you know of a way to improve this, please let me know. Sample code at GitHub.

Pseudo Authoritative Server Using Unity’s HLAPI

I was looking at Unity’s Simple Multiplayer Example. It’s rather easy to set up and runs pretty good. They mentioned it was an “authoritative server” which confused me because the client is the one deciding where it moves. I was hoping we could fix that somewhat by using clientMoveCallback3D.

The below code will take the client’s new position and check to make sure it’s not too far from its last known position on the server. We also check to make sure they haven’t moved inside anything. If they are too far or inside a wall, we’ll tell them to move back.

// Only called on one client
// Call if client was found in illegal spot
// Fix me: can this be abused?
[TargetRpc]
void TargetSetPosition(NetworkConnection target, Vector3 position)
{
    Debug.Log("Setting position to " + position);
    transform.position = position;
}

// Only called by the server
public bool ValidateMove(ref Vector3 position, ref Vector3 velocity, ref Quaternion rotation)
{
    if (position == transform.position) // Don't bother if they didn't move
        return true;

    // Did they move too far away?
    // Fix me: what if we want them to transport?
    // Fix me: what if they are falling?
    if (Vector3.Distance(transform.position, position) > 0.5f)
    {
        // Tell client to move to last known good position
        TargetSetPosition(connectionToClient, transform.position);
        return false;
    }

    // Are they moving inside anything?
    // Fix me: what if they're trapped inside something already?
    Collider[] colliders = Physics.OverlapBox(position, m_collider.bounds.extents);
    if (colliders.Length > 1)
    {
        // Tell client to move to last known good position
        TargetSetPosition(connectionToClient, transform.position);
        return false;
    }

    // Everything's good, accept client move
    return true;
}

If we return false on a ValidateMove, then the server will not update the client’s move and it won’t let other clients know about the new position either. The client that moved illegally will be in the new position on their computer, however. I’m using TargetRpcAttribute to tell the client to set themselves back to the old spot.

Here’s the updated PlayerController.cs:

using UnityEngine;
using UnityEngine.Networking;

public class PlayerController : NetworkBehaviour
{
    private Collider m_collider;
    private string m_log;

    void Start()
    {
        m_collider = GetComponent<Collider>();
    }

    // Get client input
    void FixedUpdate()
    {
        if (!isLocalPlayer)
            return;

        var x = Input.GetAxis("Horizontal") * Time.deltaTime * 150.0f;
        var z = Input.GetAxis("Vertical") * Time.deltaTime * 3.0f;
        if (Input.GetButton("Jump")) // Move fast like we're cheating
            z *= 10.0f;

        transform.Rotate(0, x, 0);
        // Don't move into things
        Collider[] colliders = Physics.OverlapBox(transform.TransformPoint(0, 0, z), m_collider.bounds.extents);
        if (colliders.Length == 1) // We'll always detect our own collider
            transform.Translate(0, 0, z);
    }

    // Only called on one client
    // Call if client was found in illegal spot
    // Fix me: can this be abused?
    [TargetRpc]
    void TargetSetPosition(NetworkConnection target, Vector3 position)
    {
        Debug.Log("Setting position to " + position);
        transform.position = position;
    }

    // Only called by the server
    public bool ValidateMove(ref Vector3 position, ref Vector3 velocity, ref Quaternion rotation)
    {
        if (position == transform.position) // Don't bother if they didn't move
            return true;

        // Did they move too far away?
        // Fix me: what if we want them to transport?
        // Fix me: what if they are falling?
        if (Vector3.Distance(transform.position, position) > 0.5f)
        {
            // Tell client to move to last known good position
            TargetSetPosition(connectionToClient, transform.position);
            return false;
        }

        // Are they moving inside anything?
        // Fix me: what if they're trapped inside something already?
        Collider[] colliders = Physics.OverlapBox(position, m_collider.bounds.extents);
        if (colliders.Length > 1)
        {
            // Tell client to move to last known good position
            TargetSetPosition(connectionToClient, transform.position);
            return false;
        }

        // Everything's good, accept client move
        return true;
    }

    public override void OnStartServer()
    {
        GetComponent<NetworkTransform>().clientMoveCallback3D = ValidateMove;
    }

    public override void OnStartLocalPlayer()
    {
        GetComponent<MeshRenderer>().material.color = Color.blue;

        // Hook up for Debug messages
        Application.logMessageReceived += HandleLog;
    }

    private void HandleLog(string condition, string stackTrace, LogType type)
    {
        if (type == LogType.Log)
        {
            if (m_log.Split('\n').Length > 20)
                m_log = "";
            m_log += "\n" + condition;
        }
    }

    public void OnGUI()
    {
        GUI.Label(new Rect(0, 0, Screen.width, Screen.height), m_log);
    }
}

If you know of a way to improve this, please let me know. Sample code at GitHub.

Simple Authoritative Server Part 2

This is part 2 of the authoritative server. In this example the client will tell the server where it wants to go (up, down, left, right), and the server in turn tells the client where its position is.

clientinput

This is written using Unity’s NetworkTransport API.

First the server will create itself and set up a routine to send out client position information every tenth of a second.

void Start()
{
    Application.runInBackground = true;
    NetworkTransport.Init();

    ConnectionConfig config = new ConnectionConfig();
    reliableChannel = config.AddChannel(QosType.Reliable);
    HostTopology topology = new HostTopology(config, 5);
#if UNITY_EDITOR
    // Listen on port 25000
    m_hostId = NetworkTransport.AddHostWithSimulator(topology, 200, 400, 25000);
#else
    m_hostId = NetworkTransport.AddHost(topology, 25000);
#endif

    // Send client position data every so often to those connected
    StartCoroutine(SendPositionCoroutine());
}

Here is how we send out each clients’ position as calculated by Unity’s physics engine. clientList is a list we keep of all clients that are connected to us and which GameObject is theirs. We send the server GameObject’s GetInstanceID() so that the clients will know who we’re talking about. Note we include the PacketTypeEnum as the first byte so that the client will know if this is a “position of all clients” packet or about the clients that are connected (name, color).

enum PacketTypeEnum
{
    Unknown = (1 << 0),
    Position = (1 << 1),
    Information = (1 << 2)
}

// Send client position data every so often to those connected
IEnumerator SendPositionCoroutine()
{
    NetworkWriter nr = new NetworkWriter();

    while (true)
    {
        yield return new WaitForSeconds(0.1f);

        // Anything to do?
        if (clientList.Count == 0)
            continue;

        // Reset stream
        nr.SeekZero();

        nr.Write((byte)PacketTypeEnum.Position);
        foreach (var item in clientList)
        {
            nr.Write(item.obj.GetInstanceID());
            nr.Write(item.obj.transform.position);

            // Don't pack too much
            // Fix me! Send more than one packet instead
            if (nr.Position > 1300)
                break;
        }

        // Send data out
        byte[] buffer = nr.ToArray();
        byte error;
        //Debug.Log(string.Format("Sending data size {0}", buffer.Length));
        foreach (var item in clientList)
        {
            NetworkTransport.Send(m_hostId, item.connectionId, reliableChannel, buffer, buffer.Length, out error);
        }
    }
}

Then the server has an Update routine where it listens for incoming traffic. It will create a GameObject when clients connect, remove one when they disconnect, and process client input here.

void Update()
{
    // Remember who's connecting and disconnecting to us
    if (m_hostId == -1)
        return;
    int connectionId;
    int channelId;
    int receivedSize;
    byte error;
    byte[] buffer = new byte[1500];
    NetworkEventType networkEvent = NetworkTransport.ReceiveFromHost(m_hostId, out connectionId, out channelId, buffer, buffer.Length, out receivedSize, out error);
    switch (networkEvent)
    {
        case NetworkEventType.Nothing:
            break;
        case NetworkEventType.ConnectEvent:
            ClientConnected(connectionId);
            break;
        case NetworkEventType.DisconnectEvent:
            ClientData cd = clientList.FirstOrDefault(item => item.connectionId == connectionId);
            if (cd != null)
            {
                Destroy(cd.obj);
                clientList.Remove(cd);
                Debug.Log("Client disconnected");
                // Send all clients new info
                SendClientInformation();
            }
            else
            {
                Debug.Log("Client disconnected that we didn't know about!?");
            }
            break;
        case NetworkEventType.DataEvent:
            //Debug.Log(string.Format("Got data size {0}", receivedSize));
            Array.Resize(ref buffer, receivedSize);
            ProcessClientInput(connectionId, buffer);
            break;
    }
}

To process client input we do this.

enum InputTypeEnum
{
    KeyNone = (1 << 0),
    KeyUp = (1 << 1),
    KeyDown = (1 << 2),
    KeyLeft = (1 << 3),
    KeyRight = (1 << 4),
    KeyJump = (1 << 5)
}

void ProcessClientInput(int connectionId, byte[] buffer)
{
    ClientData cd = clientList.FirstOrDefault(item => item.connectionId == connectionId);
    if (cd == null)
    {
        Debug.Log("Client that we didn't know about!?");
        return;
    }

    InputTypeEnum input = (InputTypeEnum)buffer[0];
    float deltaX = 0.0f;
    float deltaZ = 0.0f;
    if ((input & InputTypeEnum.KeyUp) == InputTypeEnum.KeyUp)
        deltaX = 1.0f;
    if ((input & InputTypeEnum.KeyDown) == InputTypeEnum.KeyDown)
        deltaX = -1.0f;
    if ((input & InputTypeEnum.KeyRight) == InputTypeEnum.KeyRight)
        deltaZ = 1.0f;
    if ((input & InputTypeEnum.KeyLeft) == InputTypeEnum.KeyLeft)
        deltaZ = -1.0f;
    Vector3 movement = new Vector3(deltaX, 0, deltaZ);
    movement = transform.TransformDirection(movement);
    movement *= 10.0f;
    cd.obj.GetComponent<CharacterController>().Move(movement * Time.deltaTime);
}

On the client side, our Update script will look like this:

void Update()
{
    if (m_hostId == -1)
        return;
    int connectionId;
    int channelId;
    int receivedSize;
    byte error;
    byte[] buffer = new byte[1500];
    NetworkEventType networkEvent = NetworkTransport.ReceiveFromHost(m_hostId, out connectionId, out channelId, buffer, buffer.Length, out receivedSize, out error);
    switch (networkEvent)
    {
        case NetworkEventType.Nothing:
            break;
        case NetworkEventType.ConnectEvent:
            m_serverConnectionId = connectionId;
            break;
        case NetworkEventType.DisconnectEvent:
            m_serverConnectionId = -1;
            break;
        case NetworkEventType.DataEvent:
            if (connectionId != m_serverConnectionId)
            {
                Debug.Log("Data not from server!?");
            }
            else
            {
                Array.Resize(ref buffer, receivedSize);
                ProcessServerData(buffer);
            }
            break;
    }
}

The client’s ProcessServerData is a big routine where we process PacketTypeEnum.Position:

class PositionData
{
    public int objectId;
    public Vector3 pos;
}

NetworkReader nr = new NetworkReader(buffer);
List<PositionData> posList = new List<PositionData>();
PositionData p;
while (nr.Position != buffer.Length)
{
    p = new PositionData();
    p.objectId = nr.ReadInt32();
    p.pos = nr.ReadVector3();
    posList.Add(p);
}

// Update game objects
foreach (var item in clientList)
{
    if (item.obj == null)
        continue;
    p = posList.FirstOrDefault(x => x.objectId == item.objectId);
    if (p == null)
        Debug.Log("Cannot find game object");
    else
        item.obj.transform.position = p.pos;
}

…we also process PacketTypeEnum.Information which is sent out from the server whenever a client connects or disconnects:

class InformationData
{
    public int objectId;
    public string name;
    public Vector3 pos;
    public float r;
    public float g;
    public float b;
}

NetworkReader nr = new NetworkReader(buffer);
List<InformationData> infoList = new List<InformationData>();
InformationData info;
while (nr.Position != buffer.Length)
{
    info = new InformationData();
    info.objectId = nr.ReadInt32();
    info.name = nr.ReadString();
    info.pos = nr.ReadVector3();
    info.r = nr.ReadSingle();
    info.g = nr.ReadSingle();
    info.b = nr.ReadSingle();
    infoList.Add(info);
}

// Remove clients that aren't listed
foreach (var item in clientList)
{
    if (item.obj == null)
        continue;
    info = infoList.FirstOrDefault(x => x.objectId == item.objectId);
    if (info == null)
        Destroy(item.obj);
}
clientList.RemoveAll(x => x.obj == null); // Note items are set to null only after Update!

foreach (var item in infoList)
{
    ClientData cd = clientList.FirstOrDefault(x => x.objectId == item.objectId);
    // Is this new client info?
    if (cd == null)
    {
        // Create new object
        GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
        // No CharacterController here!
        // Set position
        obj.transform.position = item.pos;
        // Set color
        obj.GetComponent<Renderer>().material.color = new Color(item.r, item.g, item.b);

        cd = new ClientData();
        cd.objectId = item.objectId;
        cd.name = item.name;
        cd.obj = obj;
        clientList.Add(cd);
        Debug.Log(string.Format("New client info for {0}", cd.name));
    }
}

The client runs a coroutine where it sends input information every tenth of a second:

// Send input data every so often
IEnumerator SendInputCoroutine()
{
    while (true)
    {
        yield return new WaitForSeconds(0.1f);

        // Anything to do?
        if (m_hostId == -1 || m_serverConnectionId == -1)
            continue;

        InputTypeEnum input = InputTypeEnum.KeyNone;
        float f;
        f = Input.GetAxis("Horizontal");
        if (f > 0.0f)
            input |= InputTypeEnum.KeyUp;
        if (f < 0.0f)
            input |= InputTypeEnum.KeyDown;
        f = Input.GetAxis("Vertical");
        if (f > 0.0f)
            input |= InputTypeEnum.KeyRight;
        if (f < 0.0f)
            input |= InputTypeEnum.KeyLeft;
        if (Input.GetKey(KeyCode.Space))
            input |= InputTypeEnum.KeyJump;

        if (input == InputTypeEnum.KeyNone)
            continue;

        // Send data out
        byte[] buffer = new byte[1];
        buffer[0] = (byte)input;
        byte error;
        NetworkTransport.Send(m_hostId, m_serverConnectionId, reliableChannel, buffer, buffer.Length, out error);
    }
}

That’s basically it! We are still not doing interpolation which is needed. But I think this is a good start.

The two main scripts are Server.cs and Client.cs.

Both projects at GitHub

Simple Authoritative Server Part 1

An authoritative server in my mind is where the clients send their key presses to the server, and the server tells the clients where their character is located. This example here is kind of like a “Hello World”, or rather just a “Hello”, if that, because I don’t have the clients sending anything to the server. The server will generate spheres and then send the sphere positions to any clients connected and the clients display the spheres for the user. Here you can see a server with two clients connected.

authservertwoclients

Now keep in mind I have no professional experience doing any of this and I don’t even know if this is the correct approach to take, but I’ve found very few examples of this kind of thing on the Internet (using Unity’s NetworkTransport). If you don’t know about NetworkTransport, read Checking out Unity’s NetworkTransport first. This does not use Unity’s High Level API because I wanted to have as much control as possible over what was sent on the network.

Let’s start. So let’s have the server spawn a sphere and add it to a list so we can keep track of everything:

IEnumerator SpawnCoroutine()
{
    // Create new object
    GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
    sphere.AddComponent<Rigidbody>();
    //sphere.GetComponent<Rigidbody>().velocity = new Vector3(UnityEngine.Random.Range(-1.0f, 1.0f), UnityEngine.Random.Range(-1.0f, 1.0f), UnityEngine.Random.Range(-1.0f, 1.0f));
    // Set position
    sphere.transform.position = new Vector3(UnityEngine.Random.Range(-2.0f, 2.0f), UnityEngine.Random.Range(-2.0f, 2.0f), UnityEngine.Random.Range(-2.0f, 2.0f));
    // Set color
    sphere.GetComponent<Renderer>().material.color = new Color(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value);

    // Save data for when we send across the net
    sphereList.Add(sphere);
    yield return new WaitForSeconds(5.0f);

    sphereList.Remove(sphere);
    Destroy(sphere);
}

Now we need another coroutine that runs every tenth of a second and sends the sphere list out to every connected client. A major complication is turning a List into a byte stream. Let’s first serialize the data:

// Serialize list
NetworkWriter nw = new NetworkWriter();
Renderer rend;

foreach (var item in sphereList)
{
    nw.Write(item.GetInstanceID());
    nw.Write(item.transform.position);
    rend = item.GetComponent<Renderer>();
    nw.Write(rend.material.color.r);
    nw.Write(rend.material.color.g);
    nw.Write(rend.material.color.b);

    // Don't pack too much
    // Fix me! Send more than one packet instead
    if (nw.Position > 1300)
        break;
}

// Send data out
byte[] buffer = nw.ToArray();
byte error;
//Debug.Log(string.Format("Sending data size {0}", buffer.Length));
foreach (var item in connectList)
{
    NetworkTransport.Send(m_hostId, item, reliableChannel, buffer, buffer.Length, out error);
}

One shortcoming in this example is the server only sends the first 50 or so spheres out. We’d need to send more than one packet if we wanted more. NetworkTransport message size is limited as it’s UDP. I wrote the client so that it can handle different packets of spheres though. I’m going to use the GetInstanceID as a “primary key” for each sphere so we know if a client has the sphere yet or not.

Now here’s how the client will deserialize:

struct NetworkData
{
    public int id;
    public Vector3 pos;
    public float r;
    public float g;
    public float b;
}

NetworkReader nr = new NetworkReader(buffer);

List<NetworkData> tmpList = new List<NetworkData>();
NetworkData tmpData;

// Read to the end
while (nr.Position != buffer.Length)
{
    // Deserialize
    tmpData.id = nr.ReadInt32();
    tmpData.pos = nr.ReadVector3();
    tmpData.r = nr.ReadSingle();
    tmpData.g = nr.ReadSingle();
    tmpData.b = nr.ReadSingle();
    tmpList.Add(tmpData);
}

How does the client then turn this list into game objects? It remembers what game objects it’s already spawned.

class SphereData
{
    public int id;
    public float lastUpdate;
    public GameObject obj;
}
// Remeber all spheres we spawned
List<SphereData> sphereList = new List<SphereData>();

Now we just take the List we deserialized earlier on the client and turn it into new spheres if we don’t have the game object already, or update the old sphere’s position:

SphereData sd;
foreach (var networkData in tmpList)
{
    sd = sphereList.FirstOrDefault(item => item.id == networkData.id);
    // Do we have this sphere already?
    if (sd != null)
    {
        // Update position
        sd.obj.transform.position = networkData.pos;
        sd.lastUpdate = Time.realtimeSinceStartup;
    }
    else
    {
        // Create new object
        GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        // No rigidbody here!
        // Set position
        sphere.transform.position = networkData.pos;
        // Set color
        sphere.GetComponent<Renderer>().material.color = new Color(networkData.r, networkData.g, networkData.b);

        sd = new SphereData();
        sd.id = networkData.id;
        sd.obj = sphere;
        sd.lastUpdate = Time.realtimeSinceStartup;
        sphereList.Add(sd);
    }
}

The client will delete spheres it hasn’t gotten info from the server about if enough time passes.

void CleanupSpheres()
{
    foreach (var item in sphereList)
    {
        // Haven't heard about sphere in a while so destroy it
        if (item.obj.gameObject != null && item.lastUpdate + 1.0f < Time.realtimeSinceStartup)
            Destroy(item.obj.gameObject); // Note this becomes null only after Update
    }
}

So there are two main scripts, Server.cs and Client.cs. I attach these to a Main Camera in their own separate projects.

Note you need to add a Sphere to the scene, but you can turn off the Sphere Collider and Mesh Renderer. This is to make it so Unity builds with the correct shaders needed for when you do spawn the spheres. Otherwise you will see pink spheres!

Also note pressing space on the server will cause a lot of Spheres to be generated.

In the future I hope to figure out a way to send more than 50 spheres to the clients, to get some interpolation going (a la what Gabriel Gambetta talks about), and eventually have clients moving the spheres around with them sending input. (It is also better to have the server send the sphere colors only once when they are created and not on each update!)

Both projects at GitHub