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.