First Person Character Controllers, Sine Wave, Clamps and Local Transform Functions
Today we will implement a first person character controller, including head bob using sine wave, independent head movement separate from the body, and a clamp on the head to stop it rotating too far.
Sine Wave for Head Bob
For first person characters, we often need to incorporate head bob. This simulates the natural sine wave of walking.
We can do this by combining a sine wave with our movement speed/input axis.
* ( * )
A better way of writing this would be:
//Declare the function to calculate the wave.
float Wave (float magnitude, float frequency)
{
return magnitude * Mathf.Sin(Time.time * frequency);
}
//Then calling this function...
transform.position = new Vector3(transform.position.x, + Wave(0.1f, 10), transform.position.z);
As is, this is a useful element to know. It will allow you to make objects move over time, like a platform in game moving back and forwards.
Sine waves becomes even more useful if we combine it our movement velocity, or to make this easier, our input axis:
transform.position = new Vector3
(
transform.position.x,
1 + (Wave(0.1f, 10) * Math.Abs(Input.GetAxis("Vertical"))),
transform.position.z
);
The format of my new Vector3 here is for readability only, you don't need to separate the three arguments on different lines. Specifically though, let's look at the line with the Wave()
function:
1 + Wave(0.1f, 10) * Math.Abs(Input.GetAxis("Vertical"))
By multiplying our wave by our input axis, we control our wave with that input axis.
Note that I used the Math
function called Abs()
. Abs()
simply returns the absolute version of a value, or specifically, the distance of a value from zero. It is used to easily convert a negative value to a positive value. This is similar to the magnitude value of a Vector3.
What does this mean for Input.GetAxis("Vertical")
then? Essentially, if we are pressing either w or s, we will return a positive number, and if we are pressing neither of those keys, we will return a zero. By multiplying our wave by zero, it essentially cancels the wave and stops the head bob.
Let's combine the Wave()
function up with the camera then. First, parent the camera onto the player by dragging it onto the player in the Unity Inspector. Then we can use the following code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public class HeadCamera : MonoBehaviour
{
float initialHeadPosition;
float Wave(float magnitude, float frequency)
{
return magnitude * Mathf.Sin(Time.time * frequency);
}
void Start()
{
initialHeadPosition = transform.localPosition.y;
}
void Update()
{
transform.localPosition = new Vector3(
transform.localPosition.x,
initialHeadPosition + Wave(0.1f, 1f) * Mathf.Abs(Input.GetAxis("Vertical")),
transform.localPosition.z);
}
}
Looking Up and Down with Mouse Movement
For our first person system, we want to enable the player to look up and down using the mouse.
To do this, we need to be able to do the following:
Increase or decrease the camera's angle (on the x-axis) when we move our mouse upwards or downwards.
Implement some minimum or maximum angle
To start this, let's make a variable to hold the angle of our camera, at the top of the class:
float rotationX;
This will be initially set to 0 (as it is not initialised), which means no rotation. However, we need to change this variable when we move our mouse up or down. We can use our Input.GetAxis()
system for this. In our Update()
message, we could add:
rotationX += Input.GetAxis("Mouse Y");
This isn't enough though. With this, our number will keep getting greater or smaller with our mouse movement. We want to have a hard limit for how low or high you can look. To do this, we need a Clamp
.
Mathf.Clamp()
allows us to define the minimum and maximum values in a variable. It contains a number outside of the range defined, it will force it back within the assigned range.
Let's look at a Clamp()
call. This would ideally also be in the Update()
message, just below our previous line of code.
rotationX = Mathf.Clamp(rotationX, -80, 80);
As Mathf.Clamp()
returns a clamped value, we need to assign our variable to whatever the function returns.
The function has three arguments:
The variable we want to check.
The minimum value it can store.
The maximum value it can store.
Imagine rotationX
held a value of 90. When we call our function, it will notice that it is outside of the specified range, and return a value of 80. Then we take that value and put it back into our rotationX
variable, which is therefore now 80.
The last thing to do is actually use that variable. Specifically, we need to set the angle of the Camera object to whatever that variable contains. As this needs to be constantly updated, it will be done in the Update() message.
Let's take a look at how we set our camera object's rotation:
transform.localRotation = Quaternion.Euler(new Vector3(rotationX, transform.localRotation.y, transform.localRotation.z));
First of all, we are setting the camera object's localRotation
not rotation
. This is because our camera is a child of the player object, and so we need it to:
Orient itself in relation to the player object's position, rotation etc.
Have any changes to position and rotation overridden by the player object.
Next, we are setting our localRotation to a function called Quaternion.Euler(). This function simply converts euler angles (the typical 0 - 359 range on the x, y and z axis) to how rotations are actually stored in Unity: as Quaternions.
The function requires a Vector3, so we pass it a new Vector3:
...new Vector3(rotationX, transform.localRotation.y, transform.localRotation.z));
Combining all of these elements up allows to have both head bob and camera controls on a first person character controller!
Reference Script: Camera Only Version
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public class HeadCamera : MonoBehaviour
{
float initialHeadPosition;
float rotationX;
float Wave(float magnitude, float frequency)
{
return magnitude * Mathf.Sin(Time.time * frequency);
}
void Start()
{
initialHeadPosition = transform.localPosition.y;
}
void Update()
{
rotationX += Input.GetAxis("Mouse Y");
rotationX = Mathf.Clamp(rotationX, -80, 80);
transform.localRotation = Quaternion.Euler(new Vector3(rotationX, transform.localRotation.y, transform.localRotation.z));
transform.localPosition = new Vector3(
transform.localPosition.x,
initialHeadPosition + Wave(0.1f, 4f) * Mathf.Abs(Input.GetAxis("Vertical")),
transform.localPosition.z);
}
}
Reference Script: Player Controller and Camera In One Class
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public class Player : MonoBehaviour
{
//Reference to the Character Controller component.
CharacterController cc;
//Vector 3 used to determine the direction we move.
Vector3 moveDir;
//Float used to modify our movement speed.
float moveSpeed = 1;
//Used to control our camera angle.
float xAxis;
//Used to control the speed of our sine wave.
float speed = 10;
//Used to control the magnitude (max distance from zero) of our sine wave.
float magnitude = 0.1f;
//Records where the camera's local position starts.
float cameraStartPos;
//Stores the main camera.
Camera headCam;
void Start()
{
//Puts this object's Character Controller component and puts it in the reference.
cc = GetComponent<CharacterController>();
//Grabs the main camera (specified via tag) and places it into the variables.
headCam = Camera.main;
//Grab the starting position of the camera and put it into a variable.
cameraStartPos = headCam.transform.position.y;
}
// Update is called once per frame
void Update()
{
//Spin the character with the mouse.
transform.Rotate(0, Input.GetAxis("Mouse X"), 0);
//Control our forward movement using the Input Axis of Unity.
moveDir = new Vector3(0, 0, Input.GetAxis("Vertical"));
//Account for the angle we are facing in our movement vector.
moveDir = transform.rotation * moveDir;
//If you hold shift down, double the movement speed.
if (Input.GetKey(KeyCode.LeftShift)) moveSpeed = 4;
else moveSpeed = 2;
//Give the character controller the move command.
cc.SimpleMove(moveDir * moveSpeed);
//If we hold shift, double our bob speed.
if (Input.GetKey(KeyCode.LeftShift)) speed = 20;
else speed = 10;
//Increase or decrease our angle by the input axis of the mouse's y position
xAxis -= Input.GetAxis("Mouse Y");
//Clamps the xAxis so it can't go above or beyond certain values.
xAxis = Mathf.Clamp(xAxis, -80, 80);
//Updates the angle of the camera, specifically the x axis.
headCam.transform.localRotation = Quaternion.Euler(xAxis, 0, 0);
//Makes the head 'bob' when moving forwards and backwards using a wave.
headCam.transform.localPosition = new Vector3(headCam.transform.localPosition.x, cameraStartPos + (Wave(magnitude, speed) * Mathf.Abs(Input.GetAxis("Vertical"))), headCam.transform.localPosition.z);
}
//Calculates a sine wave.
float Wave(float mag, float freq)
{
return (mag * Mathf.Sin(Time.time * freq));
}
}
Last updated