Super Mario Galaxy has a somewhat unique character control system where players can move in all directions. It means that characters can move on the wall seamlessly from the floor and vice versa. Gravity is not specific like in other games where the character always falls down. Mario can jump from one mini world to another mini world and start walking right away. We will be learning how can we achieve such features in our game using the Unity game engine. This should be fairly easy to understand and easily transferable to other game engines of your choice.

What are we making?
1) Make a simple gravity system so that player can fall when he jumps.
2) Player is able to move in any direction of his choice.
3) Player should be able to jump regardless of where he is standing right now.
4) Player should be able to jump from one mini world to another mini world and should be able to move there.

Creating Gravity
For creating gravity we need to first know where the player will be falling. Gravity is just a force in a certain direction (usually towards the object's centre). But in our case, we won't be applying force towards the centre of an object. In our case, we will apply force opposite of the surface normal of an object. Normal describes the orientation of a geometric object. It shows where the surface is facing. So first we calculate the normal of an object. The steps taken to calculate normal is as follow.

1) First user OverlapSphere to find all overlapping colliders in a certain radius.
2) Find a collider with the shortest distance from the player.
3) Cast raycast towards shortest distance collider.
4) Find Normal at a point where raycast hit.
5) Linearly interpolate from current normal to newfound normal to make a smooth transition in current player rotation.

Once the normal is calculated, we just apply force opposite of the current normal and we have our gravity Next we apply rotation to this player using calculated normal so that player always aligns properly on the surface. This will make sure that players will always rotate properly when we jump from one world to another,  or when we walk on different surfaces. Let's see how we can do all this on code.

public class CustomGravity : MonoBehaviour
{
   [SerializeField] private float gravityPower =20f;
   [SerializeField] private LayerMask groundMask ;
   [SerializeField] private Rigidbody rb;
   [SerializeField] private float groundCheckDistance;

   // this controls how player is rotated from current normal to next normal
   [SerializeField] private float playerSmoothRotation;
   private Vector3 newNormal;
   public Vector3 currentNormal;

   private void Start()
   {
      rb.useGravity = false; // disable this rigidbody gravity
      rb.freezeRotation = true; // disable this rigidbody from rotating with physics
   }

   private void FixedUpdate()
   {
      ApplyGravity();
   }
   private void Update()
   {
      CheckForGravityDir();
      alignToSurface();
   }
   private void ApplyGravity()
   {
      rb.AddForce(-currentNormal*gravityPower*Time.deltaTime,ForceMode.Acceleration);
   }
   
   void CheckForGravityDir()
   {
      var colliders =Physics.OverlapSphere(transform.position, groundCheckDistance);
      Vector3 closestPoint = Vector3.zero;
      float distance = Single.MaxValue;

      int totalCollision = 0;
      foreach (var collider in colliders)
      {
         // make sure to avoid checking own collider
         if (collider.gameObject.transform.root != transform)
         {
            totalCollision++;
            float newDistance = Vector3.Distance(collider.ClosestPoint(transform.position), transform.position);
            Debug.DrawLine(transform.position,collider.ClosestPoint(transform.position));
            if (newDistance < distance)
            {
               distance = newDistance;
               closestPoint = collider.ClosestPoint(transform.position);
            }
         }
         
      }
      Vector3 rayShootDir = (closestPoint - transform.position).normalized;
      bool hitSuccess = Physics.Raycast(transform.position, rayShootDir, out RaycastHit hitInfo, groundCheckDistance,
         groundMask);
      
      if (hitSuccess)
      {
         newNormal = hitInfo.normal;
         lastSurfacePos = hitInfo.point;
      }

   }
   private void alignToSurface()
   {
      // this will smooth out change from current normal to next normal
      currentNormal = Vector3.Lerp(currentNormal, newNormal, playerSmoothRotation);
      transform.up = currentNormal;
   }  
}

Player Controls
Once gravity is done, our next job is to move the player. Moving a player is fairly an easy process. We take input and then use vertical input to move a player in a current forward direction of this player model and horizontal input to rotate the player's model. For jumping, we just apply instant force towards the current normal direction so that jump is always perfect wherever we are standing right now. Let's look at code to get a better idea.

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [SerializeField] private Rigidbody rb;
    [SerializeField] private float moveSpeed = 10f, rotateSpeed= 10f;
    [SerializeField] private Transform playerModel;
    [SerializeField] private CustomGravity _customGravity;
    [SerializeField] private float jumpForce;

    private Vector3 playerInput;
    private bool jump;
    
    private void CheckInput()
    {
        playerInput.x = Input.GetAxisRaw("Horizontal");
        playerInput.z = Input.GetAxisRaw("Vertical");
        if(!jump)
        jump = Input.GetKeyDown(KeyCode.Space);
    }

    private void Update()
    {
        CheckInput();
    }

    private void FixedUpdate()
    {
        RotateCharacter();
        Move();
        if (jump)
        {
            jump = false;
            Jump();
        }
    }

    private void Move()
    {
        transform.position += playerModel.forward *playerInput.z* moveSpeed * Time.deltaTime;
    }

    private void RotateCharacter()
    {
        playerModel.Rotate(Vector3.up,playerInput.x*rotateSpeed*Time.deltaTime);
    }

    private void Jump()
    {
        rb.AddForce(_customGravity.currentNormal*jumpForce,ForceMode.VelocityChange);
    }
}

Now let's look at how my character is set up so that you can get a better idea.

Character Control Similar To Mario Galaxy: Unity Demo

This doesn't include a camera control system so that's one part you will have to do on your own. This system works properly as long as the edge is not too steep. It does work on the steep surfaces too but you won't get a smooth transition when changing the angle. And that's it, this wraps up our article. I hope you get an idea of how this system works and are able to make an even better system than this.

Thank you for following the article. I'll be writing more. Stay tuned.