Wall Jumping in Unity
The next challenge in creating the 2.5D Platformer is Wall Jumping. Wall jumping is where the character jumps up into a wall, ricochets away onto another wall, and works their way up an obstacle.
In the prototype below, there is a vertical section that the player must wall jump to overcome the challenge.
Here’s how I did it. My player is using the Character Controller component which comes with some additional capabilities.
One of these is a method similar to OnCollisionEnter() called OnControllerColliderHit(). It is called when the Controller hits something. This method will pass in a ControllerColliderHit class that contains quite a bit of information. It is possible to get the tag of the object that the Controller hit. In my example, I tagged the two vertical obstacles as “Wall”. I know that if I’m going to wall jump, I’ll need to make sure that I’m colliding with a wall.
Another piece of information I can get from the ControllerColliderHit class is the normal. A normal is a vector3 where the three axis values can range from-1 to 1. 1.0f is 0 degrees, 0.5f is 45, 0.0f is 90… This normal is the inverse of the angle that the object is hit. Using my example above, if I ran left into the wall, the normal would be, (1.00, 0.00, 0.00)
Since the normal is already inverted for us, we can use that as our rebound vector. In my player script, I created a couple of global variables, and a method to detect and store the collision information.
private bool _canWallJump = false;
Vector3 _wallSurfaceNormal;
private void OnControllerColliderHit(ControllerColliderHit hit)
{
if(!_controller.isGrounded && hit.gameObject.tag == "Wall")
{
_canWallJump = true;
_wallSurfaceNormal = hit.normal;
}
}
Inside the CalculateMovement method, I added some code to check for the spacebar being pressed and _canWallJump being true.
if (Input.GetKeyDown(KeyCode.Space) && _canWallJump == true) // Wall Jump
{
_yVelocity += _jumpHeight;
_velocity = _wallSurfaceNormal * _speed;
_canWallJump = false;
}
The last thing I had to do was to modify my double jump code to stop it from activating when I wanted to wall jump. I can double jump if I’m not wall jumping.
if (Input.GetKeyDown(KeyCode.Space) && _canWallJump == false) // Jump
{
if (_canDoubleJump == true) // Double Jump
{
_yVelocity += _jumpHeight;
_canDoubleJump = false;
}
}
Here is the complete code for the Player script.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class Player : MonoBehaviour
{
private CharacterController _controller;
[SerializeField]
private float _speed = 5.0f;
[SerializeField]
private float _gravity = 1.0f;
[SerializeField]
private float _jumpHeight = 15.0f;
private float _yVelocity;
private bool _canDoubleJump = false;
[SerializeField]
private int _coins;
private UIManager _uiManager;
[SerializeField]
private int _lives = 3;
private bool _canWallJump = false;
Vector3 _wallSurfaceNormal;
private Vector3 _direction;
private Vector3 _velocity;
void Start()
{
_controller = GetComponent<CharacterController>();
_uiManager = GameObject.Find("Canvas").GetComponent<UIManager>();
if (_uiManager == null)
{
Debug.LogError("The UI Manager is NULL.");
}
_uiManager.UpdateLivesDisplay(_lives);
}
void Update()
{
CalculateMovement();
}
private void CalculateMovement()
{
float horizontalInput = Input.GetAxis("Horizontal");
if (_controller.isGrounded == true)
{
_canWallJump = true;
_direction = new Vector3(horizontalInput, 0, 0);
_velocity = _direction * _speed;
if (Input.GetKeyDown(KeyCode.Space))
{
_yVelocity = _jumpHeight;
_canDoubleJump = true;
}
}
else
{
if (Input.GetKeyDown(KeyCode.Space) && _canWallJump == false) // Jump
{
if (_canDoubleJump == true) // Double Jump
{
_yVelocity += _jumpHeight;
_canDoubleJump = false;
}
}
if (Input.GetKeyDown(KeyCode.Space) && _canWallJump == true) // Wall Jump
{
_yVelocity += _jumpHeight;
_velocity = _wallSurfaceNormal * _speed;
_canWallJump = false;
}
_yVelocity -= _gravity;
}
_velocity.y = _yVelocity;
if (_controller.enabled == true)
{
_controller.Move(_velocity * Time.deltaTime);
}
}
public void AddCoins()
{
_coins++;
_uiManager.UpdateCoinDisplay(_coins);
}
public int GetNumberOfCoins()
{
return _coins;
}
public void Damage()
{
_lives--;
_uiManager.UpdateLivesDisplay(_lives);
if (_lives < 1)
{
SceneManager.LoadScene(0);
}
}
private void OnControllerColliderHit(ControllerColliderHit hit)
{
if(!_controller.isGrounded && hit.gameObject.tag == "Wall")
{
_canWallJump = true;
_wallSurfaceNormal = hit.normal;
}
}
}