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.

Leave a Reply

Your email address will not be published. Required fields are marked *