top of page

Smooth clamp a character controller's speed up to the screen edge

If your Character Controller in Unity can move independently from the Camera position, you'll probably want to prevent the Character Controller going off screen.


You probably want to avoid the controller getting stuck when it reaches the edge of your screen (which would happen if you tell it to stop moving when it reaches the edge of the screen.)


And you probably want it to have some smooth deceleration as the controller approaches the edge of the screen rather than come to an abrupt stop.


Here is a simple method that should provide you with a starting point.





Smooth clamp when using a gamepad stick

The crux of the approach used below to gradually decelerate the character controller as it moves towards the edge of the screen and bring it to a stop is by:

  1. Getting the distance to the edge of the screen

  2. And then using an animation curve to modify the speed of the controller.


This is the method I use to get the distance to the edge of the screen, "normalized" to a value between 0 and 0.5:

private float LowestDistanceToScreenEdge(CharacterController controller, Vector3 targetDirection)
{
	Vector3 current = controller.transform.position;
	Vector3 projected = current + (targetDirection.normalized * (currentSpeed * Time.deltaTime));
	Vector3 view = mainCamera.WorldToViewportPoint(projected);

	view.x = Mathf.Min(view.x, 1 - view.x);
    	view.y = Mathf.Min(view.y, 1 - view.y);

	return Mathf.Min(view.x, view.y); 
}

To calculate the distance to the edge of the screen, convert the expected position of the controller after the move to Viewport space coordinates using Camera.WorldToViewportPoint. In Viewport space, the bottom-left of the camera is (0,0) and the top-right is (1,1).


For the purpose of this approach, the middle of the screen should be the highest value, not the top right. So if the either the x or y values are greater than 0.5 they are modified by deducting them from 1 and using that value instead. So now the bottom-left is (0,0), the top-right is (0,0) and the middle is (0.5, 0.5);


This gets us a Vector2 position in the Viewport space but we need a distance to the edge of the screen. If we were to use both the x and y values of the Vector2 (e.g. with something like Vector2.magnitude) the corners of the screen would not be treated the same as the middle of a screen edge.


A corner would be further from the middle of the screen than a middle edge - mathematically true but not what I need. A visual representation of the controller speed would look something like this:


But what I want is something more like this:


So instead, I just take the lowest value of the x or the y and return that.


That value is then used to evaluate an animation curve. And in turn, that value is used to modify the controller's speed.


I have the curve setup like this - with time 0 returning a value of 0 and time 0.15 returning a value of 1. But you could modify this in many ways. Have it so the controller never reaches 0 in speed; allow the controller to go slightly beyond the screen before stopping; start the speed adjustmet much further from the screen edge...


Using an animation curve gives us an easy and relatively intuitive way to modify the speed of the controller.


Here is the full code:

public float currentSpeed { get; private set;}
public float maxSpeed; 
public float controllerAcceleration;
public AnimationCurve speedModifierCurve; 
public Camera mainCamera; 
private Vector3 _targetRotation; 

private void MoveWithController(CharacterController controller, InputData data)
{
	float inputMagnitude = data.analogMovement ? 1f : data.aim.magnitude;

      currentSpeed = Mathf.Lerp(currentSpeed, maxSpeed * inputMagnitude, Time.deltaTime * controllerAcceleration);

      Vector3 direction = GetTargetDirection();
            
	float distanceToEdge = LowestDistanceToScreenEdge(controller, direction);
	float speedModifier = speedModifierCurve.Evaluate(distanceToEdge); 
            
	controller.Move(direction.normalized * (currentSpeed * speedModifier * Time.deltaTime));
}

private Vector3 GetTargetDirection(InputData data)
{
	if (data.aim != Vector2.zero)
	{
		Vector3 direction = new Vector3(data.aim.x, 0.0f, data.aim.y).normalized;
		float camY = mainCamera.transform.eulerAngles.y; 
		_targetRotation = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg + camY;
	}

	return Quaternion.Euler(0.0f, _targetRotation, 0.0f) * Vector3.forward; 
}

private float LowestDistanceToScreenEdge(CharacterController controller, Vector3 targetDirection)
{
	Vector3 current = controller.transform.position;
	Vector3 projected = current + (targetDirection.normalized * (currentSpeed * Time.deltaTime));
	Vector3 view = mainCamera.WorldToViewportPoint(projected);

	if (view.x > 0.5f) view.x = 1 - view.x;
	if (view.y > 0.5f) view.y = 1 - view.y;

	if (view.x < view.y) return view.x; 
	return view.y; 
}

And with a mouse


You could easily use the same method as shown here but by getting the mouse position and evaluating it against the screen width and height.


Happy coding!

Comments


Newsletter

If you enjoyed this post, please consider signing up for our rare newsletter on the state of game development, occasional tutorials and an opportunity to participate in community game testing. 

I want to recieve:

Thanks for subscribing!

bottom of page