How to allow player to slide along wall?

I’ve cot a player controller on a GameObject. There is no Rigidbody on it. I’m doing all the movement through the PlayerController. So far I have the player controller working where I can move around, fall off ledges and bump into walls.

Currently if I bump into a wall, I set the last know good position. This effectively stops the player in his tracks, so to speak. This is what I want when the play comes head on at a wall, 90deg + or - 10 deg.

This is not what I want to happen when the player comes at the wall at a shallower angle.

If a player comes at the wall at an shallow angle, like A, I want the player to still move along the wall at a speed proportional to the angle. Additionally I would like the GameObject to rotate automatically to be like B.

The idea is the the player bumps into the wall but doesn’t come to a stop, he/she would slide along the wall and turn to face the new direction.

My current controller:

using UnityEngine;
using UnityEngine.Networking;
using System.Collections;

[RequireComponent(typeof(NetworkIdentity))]
public class PlayerController : NetworkBehaviour {
	public float size = 0.25f;
	public float speed = 8;
	public float rotateSpeed = 9;

	private Transform _transform;
	private Map _map;
	private bool _active = false;

	private Vector3 _lastPosition;
	private bool _isGrounded = false;

	void Start () {
		_transform = transform;
		Messenger.AddListener ("MAP_LOADED", OnMapLoaded);

		_transform.localPosition = new Vector3 (-100, -100, -100);

		gameObject.name = "Player " + gameObject.GetComponent<NetworkIdentity> ().netId.Value;
		_map = GameObject.Find ("Map").GetComponent<Map> ();

	}

	void OnMapLoaded () {
		if (isLocalPlayer) {
			// Hook up the camera
			PlayerCamera cam = Camera.main.GetComponent<PlayerCamera>();
			cam.target = transform;

			// Move the player to the it's spawn location
			_transform.localPosition = _map.GetPlayerSpawn();
		}

		// Set the player as active
		_active = true;
	}

	void Update () {
		if (!isLocalPlayer || !_active) {
			return;
		}

		_lastPosition = _transform.position;

		float transAmount = speed * Time.deltaTime;
		float rotateAmount = rotateSpeed * Time.deltaTime;

		if (Input.GetKey ("up")) {
			transform.Translate (0, 0, transAmount);
		}
		if (Input.GetKey ("down")) {
			transform.Translate (0, 0, -transAmount);
		}
		if (Input.GetKey ("left")) {
			transform.Rotate (0, -rotateAmount, 0);
		}
		if (Input.GetKey ("right")) {
			transform.Rotate (0, rotateAmount, 0);
		}

		Vector3 down = _transform.TransformDirection(Vector3.down);

		//
		// Check what is below us
		//
		_isGrounded = false;
		RaycastHit[] hits = Physics.RaycastAll(_transform.position, down, size + 0.001f);
		foreach (RaycastHit hit in hits) {
			if (hit.collider.gameObject.tag.ToUpper() == "GROUND") {
				_isGrounded = true;
			}
		}

		//
		// Check if we bumped into the ground/walls
		//
		Collider[] colliders = Physics.OverlapSphere(_transform.position, size + 0.001f);
		foreach (Collider collider in colliders) {
			if (collider.gameObject.tag.ToUpper() == "GROUND") {
				// Moved into the ground, not allowed
				transform.position = _lastPosition;
			}
		}

		_lastPosition = _transform.position;

		//
		// If not on the ground, fall
		//
		if (!_isGrounded) {
			_transform.position += down * Time.deltaTime * _map.gravity;
		}

		//
		// Check if we bumped into the ground/walls
		//
		_isGrounded = false;
		colliders = Physics.OverlapSphere(_transform.position, size + 0.001f);
		foreach (Collider collider in colliders) {
			if (collider.gameObject.tag.ToUpper() == "GROUND") {
				// Moved into the ground, not allowed
				transform.position = _lastPosition;
			}
		}
	}

	void OnDrawGizmos () {
		Gizmos.DrawWireSphere (_transform.position, size);
	}

}

How can I update my controller to work as described?

There are a number of ways you could do this. It looks like you have a sphere in your example, so if you can get at the collision point and normal, then you can just zero out the part of the motion after the collision that is parallel to the normal. For example, if you’re moving from a to b, and you know that at point p you collided with wall with normal n (I’m assuming a, b, and p are all the center of your collider)

c = p + (b - p) - n x dot((b - p), n)

I haven’t done this in Unity, but you should be able to use a sphere cast from a to b to get at the collision point and normal.

Another alternative is just to allow the physics system to move everything, and set your collider friction to zero, but it sounds like you’ve decided not to do this.

As for turning to face along the wall, I would consider just turning the character to face the projected (i.e. remove the y component and renormalize) velocity.

Use the normal of the collider / RayCastHit together with Unity’s ProjectOnPlane function to find the moveDirection:

moveDirection = Vector3.ProjectOnPlane(moveDirection, rayCastHit.normal);

If you’re working with collisions / colliders use collision.contacts[0].normal for the normal.

What Rory Said solve my own issue. not hard to convert

This seems to work without using a raycast: On collision, pick a direction perpendicular to normal of the surface hit and keep it until collision clears:


void Move(Vector3 movementDirection){
movementDirection.y = 0;
//modify for collision avoidance and such
movementDirection = movementDirection.normalized + currentMovementModifier.normalized * 2;
movementDirection = movementDirection.normalized * moveSpeed * Time.fixedDeltaTime;
// Move the player to it’s current position plus the movement.
rigidBody.MovePosition(transform.position + movementDirection);
}

private void OnCollisionEnter(Collision other)
{
if (other.gameObject.name != “Ground”)
{
ContactPoint contact = other.GetContact(0);
Vector3 n = contact.normal;
Vector3 option1 = new Vector3(n.z, 0, -n.x);
Vector3 option2 = -option1;
if ((option1 - lastFrameRequestedMovementDirection).sqrMagnitude < (option2 - lastFrameRequestedMovementDirection).sqrMagnitude)
{
currentMovementModifier = option1;
}
else
{
currentMovementModifier = option2;
}
}
}

private void OnCollisionExit(Collision other)
{
    if (other.gameObject.name != "Ground")
    {
        currentMovementModifier = Vector3.zero;
    }
}