Get the FULL version

Unity: Normal Walker

Unity: Normal Walker thumbnail

This Unity programming tutorial explains how to create a controllable character that appears to ‘walk’ on the surface of another object. That’s achieved by using a script that matches the normal up orientation vector of the controllable game object with the surface normal of the other 3D object. But not only the script, this post also explains how to set up a scene to make it work.

A demo project with everything discussed here is available at the end of the post both in C# and JavaScript.

The first step is to create a Cube game object, that will act as your player. To do it, just select GameObject->Create Other->New Cube:

Creating a new Cube.

Creating a new Cube game object.

Repeat this step, but this time, select Sphere, instead of Cube to create a sphere, so the player will have a object to ‘walk’ on. Scale this sphere to a much bigger size than the cube’s.

Next, select the Cube game object again, and at the Hierarchy tab, name it Player. Then, add a Character Controller component to it:

Attaching the Character Controller to the Player

Attaching the Character Controller to the 'Player' game object.

Some parameters of this components needs to be changed, such as the Skin Width and the Slope Limit. In this example, they are set to 0.5 and 180 respectively. Now, it’s necessary to place the Player game object on the top of the recently created sphere. Leave a space between the two; they must not be touching each other:

Positioning the Player above the Sphere

Positioning the Player above the Sphere.

The final step is to attach the following script to the Player game object:

  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. [RequireComponent (typeof (CharacterController))]  
  5.   
  6. public class NormalWalker : MonoBehaviour   
  7. {  
  8.     //this game object's Transform  
  9.     private Transform goTransform;  
  10.       
  11.     //the speed to move the game object  
  12.     private float speed = 6.0f;  
  13.     //the gravity  
  14.     private float gravity = 50.0f;  
  15.       
  16.     //the direction to move the character  
  17.     private Vector3 moveDirection = Vector3.zero;  
  18.     //the attached character controller  
  19.     private CharacterController cController;  
  20.       
  21.     //a ray to be cast   
  22.     private Ray ray;  
  23.     //A class that stores ray collision info  
  24.     private RaycastHit hit;  
  25.       
  26.     //a class to store the previous normal value  
  27.     private Vector3 oldNormal;  
  28.     //the threshold, to discard some of the normal value variations  
  29.     public float threshold = 0.009f;  
  30.   
  31.     // Use this for initialization  
  32.     void Start ()   
  33.     {  
  34.         //get this game object's Transform  
  35.         goTransform = this.GetComponent<Transform>();  
  36.         //get the attached CharacterController component  
  37.         cController = GetComponent<CharacterController>();  
  38.     }  
  39.       
  40.     // Update is called once per frame  
  41.     void Update ()   
  42.     {  
  43.         //cast a ray from the current game object position downward, relative to the current game object orientation  
  44.         ray = new Ray(goTransform.position, -goTransform.up);    
  45.           
  46.         //if the ray has hit something  
  47.         if(Physics.Raycast(ray.origin,ray.direction, out hit, 5))//cast the ray 5 units at the specified direction    
  48.         {    
  49.             //if the current goTransform.up.y value has passed the threshold test  
  50.             if(oldNormal.y >= goTransform.up.y + threshold || oldNormal.y <= goTransform.up.y - threshold)  
  51.             {  
  52.                 //set the up vector to match the normal of the ray's collision  
  53.                 goTransform.up = hit.normal;  
  54.             }  
  55.             //store the current hit.normal inside the oldNormal  
  56.             oldNormal =  hit.normal;  
  57.         }    
  58.           
  59.         //move the game object based on keyboard input  
  60.         moveDirection = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));  
  61.         //apply the movement relative to the attached game object orientation  
  62.         moveDirection = goTransform.TransformDirection(moveDirection);  
  63.         //apply the speed to move the game object  
  64.         moveDirection *= speed;  
  65.           
  66.         // Apply gravity down, relative to the containing game object orientation  
  67.         moveDirection.y -= gravity * Time.deltaTime * goTransform.up.y;  
  68.           
  69.         // Move the game object  
  70.         cController.Move(moveDirection * Time.deltaTime);  
  71.     }  
  72. }  

As for the script, it starts by declaring a Transform variable that will store a reference to the current game object's Transform component (line 9). The next ones are two floats: one for the speed which the player moves, and the other one for the gravity that 'glues' the player to the sphere (lines 12 and 14). The third one is a Vector3, that sets the direction in which the Player moves and a CharacterController object, that will be later used to get the attached Character Controller component (lines 17 and 19).

Then, we have the members that are going to be responsible for casting the ray and obtaining it's collision results, starting with a Ray object, that's simply a ray and a RayCastHit object which stores information about the ray collision (lines 22 and 24). Finally, there's a Vector3 named oldNormal, to store the normal.hit value from the previous frame and the float threshold, to filter which normal values should be applied to the Player game object (lines 27 and 29).

After that, the Start() method initializes both goTransform and cController variables (lines 32 through 38). Following the rest of the script, the Update() method is defined, and that's where it all happens.

Inside it, the ray object is initialized each frame, since the Player can change it's position any time one of the movement keys are pressed. The ray is being cast from the pivot of the attached game object contrary to it's up vector. This way, at the initial position, the ray is cast downwards, from the Player game object position – when the Player is upside down, the ray is cast upwards (line 44).

The next part of the Update() has a if statement that checks for a ray collision, using the ray origin and direction as parameters, as well as another parameter that defines the maximum distance that collisions should be checked. In this case, all collisions within the 5 unit range are stored at the hit object (line 47). Case an object has intersected the ray's path, line 50 is executed.

It's another if statement, that checks if the obtained normal at the collision point is smaller or greater than the threshold. Without it, the controllable object could get stuck at one of the hemispheres of the globe. Case true, the up vector of the attached game object is set to be the same as the surface's normal vector of the ray we just hit. This ensures that, when the Player is moved, it respects the Sphere surface (line 53).

Moving on, at line 60, the moveDirection variable is set to be the the same as the horizontal keyboard input, at the X and Z axis (respectively, horizontal and vertical inputs). Next, the movement is applied according to the attached game object's orientation (line 51). Finally, the speed of the game object is multiplied with the moveDirection variable applying the speed to the game object movement (line 62).

Another force must be applied to the game object: the gravity. This is done at line 67, by multiplying together the gravity value, the current deltaTime and the up vector of the game object. This makes it stay on the sphere, because, no matter what the attached game object orientation is, the gravity is always applied downwards relative to its orientation. Lastly, the Move() method from the cController is called, passing the calculated moveDirection multiplied by the deltaTime as a parameter (line 70).

And that's it. Here's what it looks like:

Example project screenshot

A screenshot from the example project.

When executing the example project, you may notice some erratic movements as the player reaches one of the collider's hemispheres, even with the threshold. This happens due to the collider's geometry. Maybe the scene in this project would work better with a geosphere. An improvement that could be applied to this script is to dynamically calculate the threshold based on the difference between normal values.

Downloads

5 Comments to “Unity: Normal Walker”

  1. Nekete says:

    Thank you I was looking for something similar since I played SMGalaxy ^_^

  2. Alexander says:

    Awesome tutorial! Is it possible to let the object rotate with AD keys? If i wanted to make wipeout-style game, where the vehicle hovers over different kind of geometry.
    I got it almost working, but every time the cube hit the next normal the local y-axis snaps back to zero.

    I don’t understand why the alignment of the local y-axis also influences the y-axis rotation?

  3. AXE says:

    Nice one! I second Alexander’s question: is there a way to make the object rotate on Y axis accordingly?

  4. Vampyr Engel says:

    Can this be done with Freiendly/Enemy A.I and vehicles?

Leave a Reply to Nekete

Post Comments RSS