Sliding along walls in top-down 2D with no diagonal movement

Hey everyone,

I’m working on my first Unity project, and it’s a top-down game in the vein of Gauntlet and other similar classics. A key aspect of the gameplay is that I’m enforcing orthogonal-only movement (no diagonals), for which I’m using a pretty simple movement script that takes the input from the axes of a joystick, figures out the angle, and zeroes out either the horizontal or vertical input before moving the rigidbody2D.velocity. I’ve chosen not to use AddForce, instead simply directly changing the velocity, because that way I don’t get the slow speed ramp-up, sliding, or diagonal movement when force is applied from two different axes.

This part works fine, and I get orthogonal-only movement.

However, the problem I’m running into is that, since I’m directly changing velocity and not using AddForce, it causes the character to stop dead in its tracks when the rigidbody is in contact with a kinematic surface, i.e. a wall. I’ve attached a 0-friction physics materials to the box colliders of the walls, but whenever the character collider runs into the wall, it gets kind of stuck there due to the fact that I’m zeroing out any movement along the axis that would allow it to slide. It’s not until I move the joystick to a sufficient angle where it zeroes out the movement in the direction of the wall that the character begins to move again.

So, this makes perfect sense given the way I’ve coded it. However, this is producing some very bad controls, as every time you hit a wall or terrain object you stop dead in your tracks until you rotate the stick past the 45-degree mark, at which point your movement starts along the other axis.

I’ve been banging my head against the wall all week about this, and so now I’m hoping you folks can help me find a solution. How can I retain enforced orthogonal movement in top-down 2D while at the same time accounting for the fact that, when the character hits a wall, it has to favor the axis that will allow it to move along that wall instead of the axis it would normally favor based on the input?

So, I ended up solving this earlier today. Here’s the code for my hero unit mover script, and you can see that my switch statement in the Move function combines with OnCollisionStay2D to help solve my sliding along walls problem.

public class MoveHero : MonoBehaviour {

	public GameObject player;
	private PlayerController pc;
	public float speed;
	public float currentSpeed;
	public bool isTouchingWallRight;
	public bool isTouchingWallLeft;
	public bool isTouchingWallUp;
	public bool isTouchingWallDown;

	// Use this for initialization
	void Start () {
		
		pc = player.GetComponent<PlayerController> (); //Gets PlayerController, where the controls are defined
		currentSpeed = speed; //Uses a secondary public variable which can be modified by a dashing script
		isTouchingWallRight = false;
		isTouchingWallLeft = false;
		isTouchingWallUp = false;
		isTouchingWallDown = false;
	}
	
	void FixedUpdate () {
		
		float moveHorizontal = Input.GetAxis (pc.horizontalAxis);
		float moveVertical = Input.GetAxis (pc.verticalAxis);

		Move (moveHorizontal, moveVertical, currentSpeed);
	}
	
	public void Move (float movehorizontal, float movevertical, float movespeed){

		HeroController herocontroller = gameObject.GetComponent<HeroController>();

		//Check to see what direction you are facing in, then, if you're touching a wall, prioritize the opposite axis's movement
		switch (herocontroller.facingDirection) {
		case Constants.RIGHT:
			if (isTouchingWallRight){ 
				movevertical = movevertical * 10;
				movespeed = movespeed / 2; //This is so you don't move along walls at full speed
			}
			break;
		case Constants.LEFT:
			if (isTouchingWallLeft){ 
				movevertical = movevertical * 10;
				movespeed = movespeed / 2;
			}
			break;
		case Constants.UP:
			if (isTouchingWallUp){ 
				movehorizontal = movehorizontal * 10;
				movespeed = movespeed / 2;
			}
			break;
		case Constants.DOWN:
			if (isTouchingWallDown){ 
				movehorizontal = movehorizontal * 10;
				movespeed = movespeed / 2;
			}
			break;
		default:
			break;
		}

		//First, figure out the angle of the joystick in degrees
		float angle = Mathf.Atan2 (movevertical, movehorizontal) * Mathf.Rad2Deg;

		//Then, figure out whether or not the joystick is being sufficiently pushed to prevent joystick inaccuracy from creating false movement
		float deadcheck = Mathf.Abs (movehorizontal) + Mathf.Abs (movevertical);

		if (deadcheck > 0.6){

		//Right
		if ((angle > -45) && (angle <= 45)) {
			herocontroller.facingDirection = Constants.RIGHT;
			movehorizontal = 1;
			movevertical = 0;
		}
		//Up
		else if ((angle > 45) && (angle <= 135)){
			herocontroller.facingDirection = Constants.UP;
			movehorizontal = 0;
			movevertical = 1;
		}
		//Down
		else if ((angle > -135) && (angle <= -45)){
			herocontroller.facingDirection = Constants.DOWN;
			movehorizontal = 0;
			movevertical = -1;
		}
		else {
			herocontroller.facingDirection = Constants.LEFT;
			movehorizontal = -1;
			movevertical = 0;
		}
		}
		else{
			movevertical = 0;
			movehorizontal = 0;
		}

		//Set the direction for the hero character to move
		Vector2 movement1 = new Vector2 (movehorizontal, movevertical);

		//Set the hero in motion by directly modifying its velocity
		rigidbody2D.velocity = movement1 * movespeed;
	}

	//OnCollisionStay2D determines if you are touching a wall, and in which direction the wall is from you, setting the correct isTouchingWall to true
	void OnCollisionStay2D(Collision2D coll){
		if (coll.gameObject.tag == "Terrain"){ 
			Vector3 contactPoint = coll.contacts[0].point;
			Vector3 center = coll.collider.bounds.center;
			isTouchingWallLeft = contactPoint.x > center.x;
			isTouchingWallRight = contactPoint.x < center.x;
			isTouchingWallUp = contactPoint.y < center.y;
			isTouchingWallDown = contactPoint.y > center.y; 
		}
	}

	//OnCollisionExit2D is used to reset the isTouchingWall variables once you leave contact with a wall
	void OnCollisionExit2D(Collision2D coll){
		if (coll.gameObject.tag == "Terrain") {
			isTouchingWallLeft = false;
			isTouchingWallRight = false;
			isTouchingWallUp = false;
			isTouchingWallDown = false;
		}
	}

}