Basic movement. Walking on walls.

Hi I’ve been trying to figure this one out for a few days now and I just can’t do it.

I’m trying to make a robot game where the player walks on the outside of space stations. What I’m trying to do is have the robot walk along a surface but also be able to detach and float in empty space. I’ve made games before using the character controllers but I’ve found them to be severely lacking in flexibility.

I’ve figured out how to make the character move in space with a rigidbody and applying local forces but I can’t make it stick to a surface. I’ve tried with raycasts getting the normal and using quaternion FromToRotation(or whatever it’s called) to align my robot but it just teleports to a weird angle.

So the question: How can I make my rigidbody with cube collider align to the surface of another object and then move around on the surface of that object?

The CharacterController doesn’t work in this case because it needs Y to be the normal direction. A good way to do this is to use a rigidbody (uncheck Use Gravity), and apply a local gravity in the form of a constant force opposite to the character normal direction - the character normal must be updated using a raycast to the down side.

This script does this. I created an empty object, added a box collider and a rigidbody (useGravity = false), then childed my model to it. The terrain normal is constantly determined with the downside raycast, and smoothly updates myNormal (the character normal). myNormal is used to create the local gravity, so the character is always being attracted to the surface under its feet. In order to align the character to myNormal without loosing its current forward direction, a smart trick is used (thanks to -T- for that!): the current forward is found from the cross product between transform.right and myNormal, and the desired rotation is calculated with LookRotation(new forward, myNormal) - this returns a rotation that keeps the character looking forward and with its head pointing in myNormal direction.The weight force is applied at FixedUpdate to make it strictly constant. The character moves with WS using Translate, and can even jump to its vertical direction.

ADDED FEATURE: When jump is pressed, the character casts a forward ray; if the ray hits any wall in the jumpRange distance, the character jumps and rotates nicelly to land in this wall - much like someone with magnetic boots in the outer space.

EDITED: The original algorithm used had a big problem when the character was fully upside down: it started to flip back/forth at random points, driving us crazy. Thanks to a Boo script suggested by -T-, the character now keeps its forward direction under all circumstances, and call walk on the roof or on spherical planets like expected.

var moveSpeed: float = 6; // move speed
var turnSpeed: float = 90; // turning speed (degrees/second)
var lerpSpeed: float = 10; // smoothing speed
var gravity: float = 10; // gravity acceleration
var isGrounded: boolean;
var deltaGround: float = 0.2; // character is grounded up to this distance
var jumpSpeed: float = 10; // vertical jump initial speed
var jumpRange: float = 10; // range to detect target wall

private var surfaceNormal: Vector3; // current surface normal
private var myNormal: Vector3; // character normal
private var distGround: float; // distance from character position to ground
private var jumping = false; // flag "I'm jumping to wall"
private var vertSpeed: float = 0; // vertical jump current speed 

function Start(){
    myNormal = transform.up; // normal starts as character up direction 
    rigidbody.freezeRotation = true; // disable physics rotation
    // distance from transform.position to ground
    distGround = collider.bounds.extents.y - collider.center.y;  
}

function FixedUpdate(){
    // apply constant weight force according to character normal:
    rigidbody.AddForce(-gravity*rigidbody.mass*myNormal);
}

function Update(){
    // jump code - jump to wall or simple jump
    if (jumping) return;  // abort Update while jumping to a wall
    var ray: Ray;
    var hit: RaycastHit;
    if (Input.GetButtonDown("Jump")){ // jump pressed:
        ray = Ray(transform.position, transform.forward);
        if (Physics.Raycast(ray, hit, jumpRange)){ // wall ahead?
            JumpToWall(hit.point, hit.normal); // yes: jump to the wall
        }
        else if (isGrounded){ // no: if grounded, jump up
            rigidbody.velocity += jumpSpeed * myNormal;
        }                
    }
    
    // movement code - turn left/right with Horizontal axis:
    transform.Rotate(0, Input.GetAxis("Horizontal")*turnSpeed*Time.deltaTime, 0);
    // update surface normal and isGrounded:
    ray = Ray(transform.position, -myNormal); // cast ray downwards
    if (Physics.Raycast(ray, hit)){ // use it to update myNormal and isGrounded
        isGrounded = hit.distance <= distGround + deltaGround;
        surfaceNormal = hit.normal;
    }
    else {
        isGrounded = false;
        // assume usual ground normal to avoid "falling forever"
        surfaceNormal = Vector3.up; 
    }
    myNormal = Vector3.Lerp(myNormal, surfaceNormal, lerpSpeed*Time.deltaTime);
    // find forward direction with new myNormal:
    var myForward = Vector3.Cross(transform.right, myNormal);
    // align character to the new myNormal while keeping the forward direction:
    var targetRot = Quaternion.LookRotation(myForward, myNormal);
    transform.rotation = Quaternion.Lerp(transform.rotation, targetRot, lerpSpeed*Time.deltaTime);
    // move the character forth/back with Vertical axis:
    transform.Translate(0, 0, Input.GetAxis("Vertical")*moveSpeed*Time.deltaTime); 
}

function JumpToWall(point: Vector3, normal: Vector3){
    // jump to wall 
    jumping = true; // signal it's jumping to wall
    rigidbody.isKinematic = true; // disable physics while jumping
    var orgPos = transform.position;
    var orgRot = transform.rotation;
    var dstPos = point + normal * (distGround + 0.5); // will jump to 0.5 above wall
    var myForward = Vector3.Cross(transform.right, normal);
    var dstRot = Quaternion.LookRotation(myForward, normal);
    for (var t: float = 0.0; t < 1.0; ){
        t += Time.deltaTime;
        transform.position = Vector3.Lerp(orgPos, dstPos, t);
        transform.rotation = Quaternion.Slerp(orgRot, dstRot, t);
        yield; // return here next frame
    }
    myNormal = normal; // update myNormal
    rigidbody.isKinematic = false; // enable physics
    jumping = false; // jumping to wall finished
}

@aldonaletto Thank you for this! A big part of my game just went from conceptual to playable now that I can walk around on my “Asteroid”.

PS I had to make this an answer as it exceeds the max comment size I guess.

For anyone looking for this in C#, I translated it to:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// C# translation from http://answers.unity3d.com/questions/155907/basic-movement-walking-on-walls.html
/// Author: UA @aldonaletto 
/// </summary>

// Prequisites: create an empty GameObject, attach to it a Rigidbody w/ UseGravity unchecked
// To empty GO also add BoxCollider and this script. Makes this the parent of the Player
// Size BoxCollider to fit around Player model.

public class QM_CharController : MonoBehaviour {

	private float moveSpeed = 6; // move speed
	private float turnSpeed = 90; // turning speed (degrees/second)
	private float lerpSpeed = 10; // smoothing speed
	private float gravity = 10; // gravity acceleration
	private bool isGrounded;
	private float deltaGround = 0.2f; // character is grounded up to this distance
	private float jumpSpeed = 10; // vertical jump initial speed
	private float jumpRange = 10; // range to detect target wall
	private Vector3 surfaceNormal; // current surface normal
	private Vector3 myNormal; // character normal
	private float distGround; // distance from character position to ground
	private bool jumping = false; // flag "I'm jumping to wall"
	private float vertSpeed = 0; // vertical jump current speed

	private Transform myTransform;
	public BoxCollider boxCollider; // drag BoxCollider ref in editor

	private void Start(){
		myNormal = transform.up; // normal starts as character up direction
		myTransform = transform;
		rigidbody.freezeRotation = true; // disable physics rotation
		// distance from transform.position to ground
		distGround = boxCollider.extents.y - boxCollider.center.y;

	}

	private void FixedUpdate(){
	// apply constant weight force according to character normal:
	rigidbody.AddForce(-gravity*rigidbody.mass*myNormal);
}

    private void Update(){
	// jump code - jump to wall or simple jump
	if (jumping) return; // abort Update while jumping to a wall

		Ray ray;
		RaycastHit hit;

	if (Input.GetButtonDown("Jump")){ // jump pressed:
		ray = new Ray(myTransform.position, myTransform.forward);
		if (Physics.Raycast(ray, out hit, jumpRange)){ // wall ahead?
			JumpToWall(hit.point, hit.normal); // yes: jump to the wall
		}
		else if (isGrounded){ // no: if grounded, jump up
			rigidbody.velocity += jumpSpeed * myNormal;
		}
	}
	
	// movement code - turn left/right with Horizontal axis:
	myTransform.Rotate(0, Input.GetAxis("Horizontal")*turnSpeed*Time.deltaTime, 0);
	// update surface normal and isGrounded:
	ray = new Ray(myTransform.position, -myNormal); // cast ray downwards
	if (Physics.Raycast(ray, out hit)){ // use it to update myNormal and isGrounded
		isGrounded = hit.distance <= distGround + deltaGround;
		surfaceNormal = hit.normal;
	}
	else {
		isGrounded = false;
		// assume usual ground normal to avoid "falling forever"
		surfaceNormal = Vector3.up;
	}
	myNormal = Vector3.Lerp(myNormal, surfaceNormal, lerpSpeed*Time.deltaTime);
	// find forward direction with new myNormal:
	Vector3 myForward = Vector3.Cross(myTransform.right, myNormal);
	// align character to the new myNormal while keeping the forward direction:
	Quaternion targetRot = Quaternion.LookRotation(myForward, myNormal);
	myTransform.rotation = Quaternion.Lerp(myTransform.rotation, targetRot, lerpSpeed*Time.deltaTime);
	// move the character forth/back with Vertical axis:
	myTransform.Translate(0, 0, Input.GetAxis("Vertical")*moveSpeed*Time.deltaTime);
}

	private void JumpToWall(Vector3 point, Vector3 normal){
	// jump to wall
	jumping = true; // signal it's jumping to wall
	rigidbody.isKinematic = true; // disable physics while jumping
	Vector3 orgPos = myTransform.position;
	Quaternion orgRot = myTransform.rotation;
	Vector3 dstPos = point + normal * (distGround + 0.5f); // will jump to 0.5 above wall
	Vector3 myForward = Vector3.Cross(myTransform.right, normal);
	Quaternion dstRot = Quaternion.LookRotation(myForward, normal);
	
		StartCoroutine (jumpTime (orgPos, orgRot, dstPos, dstRot, normal));
		//jumptime
}

	private IEnumerator jumpTime(Vector3 orgPos, Quaternion orgRot, Vector3 dstPos, Quaternion dstRot, Vector3 normal) {
		for (float t = 0.0f; t < 1.0f; ){
			t += Time.deltaTime;
			myTransform.position = Vector3.Lerp(orgPos, dstPos, t);
			myTransform.rotation = Quaternion.Slerp(orgRot, dstRot, t);
			yield return null; // return here next frame
		}
		myNormal = normal; // update myNormal
		rigidbody.isKinematic = false; // enable physics
		jumping = false; // jumping to wall finished

		}

}

i know one of my friends who made a spider game literally rotated the whole level, making the walls turn into floors. not sure if its the best way, but it looked good

There’s an example on the iTween site called Moving on Uneven Terrain that might be what you’re looking for.

There’s another question that addresses the same issue here. I posted an answer to that one (two, actually, but one just points back here).

I’ve translated this code to C# and tried to adapt it for a 2D game for mobile. It rune fairly smoothly, but after some time, the avatar starts turning slowly inwards. The avatar’s rotation at this point should only be affected by the collider’s normal. There’s nothing wrong with my collider, as its normal always points in the right direction. As of now, the code looks something like this (though I’ll skip the jumping - that’s not a problem for now):

void Start(){
	rb = this.GetComponent<Rigidbody>();
	myColl = this.GetComponent<Collider>();

	myNormal = this.transform.up;
	rb.freezeRotation = true;
	distGround = myColl.bounds.extents.y - myColl.bounds.center.y;
}

void Update(){
	rb.AddForce (-gravity * rb.mass * myNormal * Time.deltaTime); //I put the gravity here because otherwise the avatar would fly off the end of a collider rather than go around it.
	if (isJumping) return;

	Ray ray;
	RaycastHit hit;

	ray = new Ray (transform.position, -myNormal);
	if (Physics.Raycast (ray, out hit)){
		isGrounded = hit.distance <= -distGround * deltaGround;
		surfaceNormal = hit.normal;
	}
	else{ 
		isGrounded = false;
		surfaceNormal = Vector3.up;
	}

	myNormal = Vector3.Lerp (myNormal, surfaceNormal, lerpSpeed * 2 * Time.deltaTime);
	Vector3 myForward = Vector3.Cross (transform.right, myNormal);
	Quaternion targetRot = Quaternion.LookRotation (myForward, myNormal);
	transform.rotation = targetRot; //lerping wasn't tight enough

	if (Joystick.IsDragging){
		transform.Translate (Joystick.HorizontalAxis*moveSpeed*direction*Time.deltaTime,0f,0f);
	}
	this.transform.position = new Vector3 (this.transform.position.x, this.transform.position.y, 0f);
}

Firstly thank you very much to @aldonaletto and @-T- for your work on the code posted here it gave me the majority of what I needed to get the same code to work for me in Unity 5.3.2. I will shortly be working on getting this code to work with mouselook also.

public var moveSpeed : float = 6.0;
public var turnSpeed : float = 90.0;
public var lerpSpeed : float = 10.0;
public var gravity : float = 10.0;
public var deltaGround : float = 0.2;
public var jumpSpeed : float = 10;
public var jumpRange : float = 10;

var isGrounded : boolean = true; 

private var surfaceNormal : Vector3;
private var myNormal : Vector3;
private var distGround : float;
private var jumping = false;
private var vertSpeed : float = 0.0;

function Start() {
	// A recent Unity update means you have to use the next
	// 2 lines to access components of objects
	var rb = GetComponent.<Rigidbody>();
	var coll = GetComponent.<Collider>();
	myNormal = transform.up;
	rb.freezeRotation = true;
	distGround = coll.bounds.extents.y;
}

function FixedUpdate() {
	var rb = GetComponent.<Rigidbody>();
	rb.AddForce(-gravity * rb.mass * myNormal);
}

function Update() {
	if (jumping) return;
	var ray : Ray;
	var hit : RaycastHit;
	if (Input.GetButtonDown("Jump")) {
		ray = Ray(transform.position, transform.forward);
		if (Physics.Raycast(ray, hit, jumpRange)) {
			JumpToWall(hit.point, hit.normal);
		}
		else if (isGrounded) {
			var rb = GetComponent.<Rigidbody>();
			rb.velocity += jumpSpeed * myNormal;
		}
	}

	transform.Rotate(0, Input.GetAxis("Horizontal") * turnSpeed * Time.deltaTime, 0);
	ray = Ray(transform.position, transform.up * -1);
	if (Physics.Raycast(ray, hit)) {
		isGrounded = hit.distance <= distGround + deltaGround;
		surfaceNormal = hit.normal;
	}
	else {
		isGrounded = false;
		surfaceNormal = Vector3.up;
	}
	myNormal = Vector3.Lerp(myNormal, surfaceNormal, lerpSpeed * Time.deltaTime);
	var myForward = Vector3.Cross(transform.right, myNormal);
	var targetRot = Quaternion.LookRotation(myForward, myNormal);
	transform.rotation = Quaternion.Lerp(transform.rotation, targetRot, lerpSpeed * Time.deltaTime);

	transform.Translate(0, 0, Input.GetAxis("Vertical") * moveSpeed * Time.deltaTime);
}

function JumpToWall(point : Vector3, normal : Vector3) {
	jumping = true;
	var rb = GetComponent.<Rigidbody>();
	rb.isKinematic = true;
	var orgPos = transform.position;
	var orgRot = transform.rotation;
	var dstPos = point + normal * (distGround + 0.5);
	var myForward = Vector3.Cross(transform.right, normal);
	var dstRot = Quaternion.LookRotation(myForward, normal);
	for (var t : float = 0.0; t < 1.0; ) {
		t += Time.deltaTime;
		transform.position = Vector3.Lerp(orgPos, dstPos, t);
		transform.rotation = Quaternion.Slerp(orgRot, dstRot, t);
		yield;
	}
	myNormal = normal;
	rb.isKinematic = false;
	jumping = false;
}

@Oableo you may be interested in this post?

Ran out of space so removed my comments, ask if you need them, but they are very similar to @aldonaletto in his accepted answer.

Anyone converted this great answer into 2d? Many thanks!,Anyone convert this great answer into 2d? Many thanks!