2. Overcharging the Game

Today we are going to Overcharge our “First 3D Game” in Unity 🙂

To be more precise, we are going to improve our “Player Controller” code to add “Special Abilities” to our character such as “Dash“, “Double Jump“, “Run“, “Glide“, “Wall Run” and “Wall Jump“.

Unity Tutorial Level: Intermediate.
“Ads”

2.1 Changing the Code.

Spoiler Alert “There’s a lot of Code coming” 🙁

In order to add double jump, dash, wall-run, glide, and dash abilities to our character (in addition to improving our basic movement), we’re going to completely overhaul our PlayerController code so it looks like the following (we’ve added as many comments as possible to help explain what each part does). So, let’s get started:

using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
    // =================================================
    // EDITABLE VARIABLES IN THE INSPECTOR
    // =================================================
    
    [Header("General Config")]
    public float moveSpeed = 5f;           // Base Movement Speed
    public float jumpForce = 5f;           // Normal jump force
    public float risingGravity = -15f;     // Force of gravity when jumping
    public float fallingGravity = -25f;    // Force of gravity when falling
    public Transform cameraTransform;      // Main camera transform
    public float groundCheckRadius = 0.4f; // Ground detection radius
    
    [Header("Components")]
    public Transform groundCheck;          // Object to detect ground
    public LayerMask groundMask;           // Layers considered as ground

    [Header("Abilities")]
    public bool canDoubleJump = false;     // Double jump
    public bool canDash = false;           // Dash
    public bool canWallJump = false;       // Wall jump
    public bool canWallRun = false;        // Wall run
    public bool canGlide = false;          // Glide
    public bool canSprint = false;         // Sprint

    [Header("Abilities Values")]
    public float sprintSpeed = 10f;         // Sprint speed
    public float dashSpeed = 30f;           // Dash speed
    public float dashDuration = 0.15f;      // Dash duration
    public float dashCooldown = 0.75f;      // Time between dashes
    public bool allowAirDash = true;        // Is air dash allowed?
    public float wallJumpForce = 15f;       // Wall jump force
    public float wallRunGravity = 1f;       // Gravity reduction during wall run
    public float wallRunSpeed = 11f;        // Speed during wall run
    public float glideFallSpeed = -1.75f;   // Falling speed during gliding

    // =================================================
    // VARIABLES PRIVADAS
    // =================================================
    
    private CharacterController controller; // CharacterController Component
    private Vector3 velocity;               // Player's current speed
    private bool isGrounded;                // Is it touching the ground?
    private int jumpCount = 0;              // Counter for double jump
    private bool isDashing = false;         // Currently in dash?
    private float dashTimer = 0f;           // Dash Timer
    private bool isWallRunning = false;     // Currently on wall run?
    private bool isGliding = false;         // Currently gliding?
    private Vector3 wallNormal;             // Normal of the current wall
    private bool isTouchingWall = false;    // Touching a wall?
    private float lastDashTime = -10f;      // Last dash time
    private bool hasAirDashed = false;      // Has dash been used in the air yet?


    // =================================================
    // INITIALIZATION
    // =================================================
    
    void Start()
    {
        // Get CharacterController component
        controller = GetComponent<CharacterController>();
        // Hide and lock cursor
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }

    // =================================================
    // MAIN LOOP
    // =================================================
    
    void Update()
    {
        // Check basic states
        CheckGround();
        CheckWalls();

        // Handle abilities
        HandleMovement();
        HandleJump();
        HandleDash();
        HandleWallRun();
        HandleGlide();
    }

    // =================================================
    // DETECTION METHODS
    // =================================================
    
    
    /// Checks if player is grounded    
    private void CheckGround()
    {
        // Detect ground collision using physics sphere
        // Parameters:
        //   * Position: groundCheck position (player's feet)
        //   * Radius: groundCheckRadius (detection sphere size)
        //   * Layer: groundMask (only detect ground layers)
        isGrounded = Physics.CheckSphere(groundCheck.position, groundCheckRadius, groundMask);

        // Reset states when grounded
        if (isGrounded)
        {
            // Reset jump counter (allows new basic jump)
            jumpCount = 0;
            // Reset air dash (allows air dash in next jump)
            hasAirDashed = false;
            // Reset horizontal velocity if not dashing
            // - Maintains dash momentum
            if(!isDashing){
                velocity.x = 0; // Stop X axis movement
                velocity.z = 0; // Stop Z axis movement
            }
            
        }
    }

    
    /// Detects nearby walls in 4 main directions and stores their normal using SphereCast   
    private void CheckWalls()
    {
        isTouchingWall = false; // Reset wall touch state
        float detectionDistance = 1.2f; // Detection distance
        
        // Directions to check (forward, right, left, back)
        Vector3[] directions = new Vector3[]
        {
            transform.forward, // Forward (positive Z)
            transform.right,   // Right (positive X)
            -transform.right,  // Left (negative X)
            -transform.forward // Back (negative Z)
        };

        // Check all directions
        RaycastHit hit;
        foreach(Vector3 dir in directions)
        {
            // Perform SphereCast in current direction
            if(Physics.SphereCast(
                transform.position, // Origin at player center
                0.5f,               // Sphere radius (half player width)
                dir,                // Check direction
                out hit,            // Collision info
                detectionDistance,  // Max detection distance
                groundMask          // Only detect ground/wall layers
            ) && Vector3.Angle(hit.normal, Vector3.up) > 50f) // Exclude floors
            // Filter flat surfaces (floors/ceilings)
            // - Angle between surface normal and vertical axis
            // - >50° considered as wall (avoids detecting slopes)
            {
                isTouchingWall = true; // Set wall contact
                wallNormal = hit.normal; // Store wall normal
                return; // Exit after first valid detection
            }
        }
    }

    // =================================================
    // MOVEMENT SYSTEM
    // =================================================
    
    
    /// Handles basic movement and sprint      
    private void HandleMovement()
    {
        if (isDashing) return; // Ignore normal movement during dash

        // Get horizontal/vertical axis inputs (WASD keys or joystick and values ​​between [-1, 1])
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        
        // Calculate direction of movement relative to the camera
        // - Converts inputs into a 3D vector oriented according to the camera rotation
        Vector3 moveDirection = CalculateCameraMovement(horizontal, vertical);
        
        // Determine current speed considering sprint 
        // - Uses sprintSpeed ​​if sprint button is held and enabled 
        // - Keeps moveSpeed ​​as base value otherwise
        float currentSpeed = canSprint && Input.GetButton("Sprint") ? sprintSpeed : moveSpeed;

        // Apply horizontal movement only if not in wall run 
        // - Avoid conflict between movement systems
        if(!isWallRunning)
        {
            // Move the CharacterController in the calculated direction 
            // - Multiply by deltaTime to make it frame-rate independent
            controller.Move(moveDirection * currentSpeed * Time.deltaTime);
        }

        // Calculate gravity based on state (ascent/descent) 
        // - fallingGravity: Strong gravity during fall or in the air 
        // - risingGravity: Soft gravity during ascent
        float currentGravity = (velocity.y < 0 || !isGrounded) ? fallingGravity : risingGravity ;

        // Apply gravity only if not in wall run/glide
        if(!isWallRunning && !isGliding)
        {
            velocity.y += currentGravity * Time.deltaTime;
        }
        
        // Apply accumulated vertical velocity 
        // - Independent movement from horizontal for better control
        controller.Move(velocity * Time.deltaTime);
    }

    
    /// Calculate direction of movement relative to the camera
    private Vector3 CalculateCameraMovement(float horizontal, float vertical)
    {
        // Get base directional vectors from the camera
        Vector3 cameraForward = cameraTransform.forward; // Vector where the camera is pointing
        Vector3 cameraRight = cameraTransform.right; // Right camera vector
        
        // Flatten vectors to ignore vertical component (Y axis) 
        // - Prevent unwanted movement when looking up/down 
        // Keep only X and Z components
        cameraForward.y = 0;
        cameraRight.y = 0;
        
        // Normalize vectors to maintain constant speed 
        // - Prevents diagonal movements from being faster
        cameraForward.Normalize(); // Longitud = 1
        cameraRight.Normalize();

        // Combine directions based on player input 
        // - Vertical: Controls forward/backward movement (Z axis) 
        // - Horizontal: Controls lateral movement (X axis)
        Vector3 combinedDirection = (cameraForward * vertical) + (cameraRight * horizontal);

        // Normalize final result to: 
        // - Maintain constant speed in all directions 
        // - Avoid values ​​greater than 1 in diagonal movement
        return combinedDirection.normalized;
    }

    // =================================================
    // JUMP SYSTEM
    // =================================================
    
    
    /// Handles all jump types
    private void HandleJump()
    {
        // Detect that the Jump button has been pressed
        if(Input.GetButtonDown("Jump"))
        {
            isGliding = false; // Cancel any active glide state

            // Normal jump/double jump
            if(isGrounded || (canDoubleJump && jumpCount < 1))
            {
                // Physical formula for jump height: v = √(2 * force * gravity) 
                // - risingGravity is used for calculation consistent with the gravity system
                velocity.y = Mathf.Sqrt(jumpForce * -2 * risingGravity);
                jumpCount++; // Increase air jump counter
            }
            // Wall jump
            else if(canWallJump && isTouchingWall)
            {
                // Calculate optimal direction (away from wall + vertical) 
                // - Vector.up * 2: Predominant vertical thrust 
                // - wallNormal: Direction perpendicular to the wall for horizontal thrust
                Vector3 jumpDirection = (Vector3.up * 2f + wallNormal).normalized;
                
                // Apply force with momentum
                velocity = jumpDirection * wallJumpForce;
                
                // Reset states
                jumpCount = 0;
                hasAirDashed = false; // Reactivate air dash
                
                // Rotate character to look away from the wall 
                // - LookRotation(-wallNormal): Creates rotation away from the wall
                transform.rotation = Quaternion.LookRotation(-wallNormal);
            }
        }

    }

    // =================================================
    // DASH SYSTEM
    // =================================================
    
    
    /// Handles dash movement    
    private void HandleDash()
    {
        // Can dash in the air? (Active skill + in the air + hasn't used air dash)
        bool canAirDash = allowAirDash && !isGrounded && !hasAirDashed;
        // Can dash on the ground? (On the ground + cooldown completed)
        bool canGroundDash = isGrounded && Time.time >= lastDashTime + dashCooldown;
        
        // Start Dash if the button is pressed and the conditions are met
        if (canDash && Input.GetButtonDown("Fire2") && (canGroundDash || canAirDash))
        {
            // Set variables
            isDashing = true; // Activate dash flag
            dashTimer = dashDuration; // Start timer
            lastDashTime = Time.time; // Record time of last dash
            
            // Register air dash (only once per jump)
            if(!isGrounded) hasAirDashed = true;

            // Calculate dash direction
            Vector3 dashDirection = GetDashDirection();

            // Apply dash force (combine with existing vertical speed)
            velocity = dashDirection * dashSpeed; // Calculated direction speed
            velocity.y = isGrounded ? 0 : velocity.y; // On the ground: purely horizontal dash
        }

        // Execute logic during the dash
        if (isDashing)
        {
            dashTimer -= Time.deltaTime; // Reduce timer

            // End dash when time expires
            if (dashTimer <= 0)
            {
                isDashing = false;
                velocity = Vector3.zero; // Delete residual velocity
            }

            //Apply dash motion (Frame-rate independent motion)
            controller.Move(velocity * Time.deltaTime);
        }
    }

    // Calculate the dash direction based on the input and the camera
    private Vector3 GetDashDirection()
    {
        // Get RAW input (without Unity anti-aliasing) for immediate steering
        float horizontal = Input.GetAxisRaw("Horizontal"); // Values: -1, 0, 1
        float vertical = Input.GetAxisRaw("Vertical"); // Values: -1, 0, 1
        
        // Calculate direction relative to the camera (same as normal movement)
        Vector3 cameraForward = cameraTransform.forward;
        Vector3 cameraRight = cameraTransform.right;
        // Ignore vertical component for horizontal movement
        cameraForward.y = 0f;
        cameraRight.y = 0f;
        // Normalize vectors to maintain constant velocity
        cameraForward.Normalize();
        cameraRight.Normalize();

        // Combine addresses based on input and normalize to avoid extra speed on diagonals
        Vector3 dashDirection = (cameraForward * vertical + cameraRight * horizontal).normalized;

        // If no input, use camera's front direction
        if(dashDirection == Vector3.zero)
        {
            dashDirection = cameraForward; // Dash forward by default
        }

        return dashDirection;

    }



    // =================================================
    // WALL RUN SYSTEM
    // =================================================
    
    
    /// Handles movement on walls    
    private void HandleWallRun()
    {   
        // Start the wall run if the corresponding button is pressed and the conditions are met
        if(canWallRun && !isGrounded && isTouchingWall && Input.GetButton("Sprint"))
        {
            isWallRunning = true;

            // Auto-rotate towards wall: 
            // - Smoothly interpolates the current rotation to the opposite direction of the wall normal 
            // - Quaternion.LookRotation(-wallNormal): Creates rotation looking away from the wall 
            // - 10f * Time.deltaTime: Rotation rate (10 degrees/second)
            transform.rotation = Quaternion.Lerp(
            transform.rotation,
            Quaternion.LookRotation(-wallNormal),
            10f * Time.deltaTime
            );
            
            // Calculate movement directions: 
            // "Front" direction parallel to the wall (90° to the normal)
            Vector3 wallForward = Vector3.Cross(wallNormal, Vector3.up);
            // Project camera direction onto the wall plane 
            // - Vector3.ProjectOnPlane: Removes component perpendicular to the wall
            // - .normalized: Maintains constant speed
            Vector3 cameraRelative = Vector3.ProjectOnPlane(cameraTransform.forward,wallNormal).normalized;
            
            // Move in combined direction (wall + input)
            float horizontal = Input.GetAxis("Horizontal");
            float vertical = Input.GetAxis("Vertical");
            // Combine directions: 
            // - vertical * cameraRelative: Forward/backward movement relative to the camera 
            // - horizontal * transform.right: Lateral movement relative to the character's rotation
            Vector3 moveDirection = (cameraRelative * vertical + transform.right * horizontal) * wallRunSpeed;
            
            // Apply movement:
            controller.Move(moveDirection * Time.deltaTime); // Horizontal movement
            // Set custom vertical gravity: 
            // - Negative value to keep the character "stuck" to the wall 
            // - wallRunGravity controls the sliding speed
            velocity.y = -wallRunGravity;
        }
        else
        {
            isWallRunning = false; // Disable wall run if conditions are not met
        }
    }

    // =================================================
    // GLIDING SYSTEM
    // =================================================
    
    
    /// Handles aerial gliding   
    private void HandleGlide()
    {
        //Start gliding if the corresponding button is held down and the required conditions are met
        if (canGlide && !isGrounded && Input.GetButton("Jump") && !isGliding && velocity.y<0)
        {
            isGliding = true;
            velocity.y = glideFallSpeed; //Modify the speed on the vertical axis to use the defined value
        }
    // Cancel glide by touching the ground or releasing the button
    else if (isGrounded || Input.GetButtonUp("Jump"))
        {
            isGliding = false;
        }
    }
}

 

“Ads”

2.2 Configuring Controls.

As you could see in the previous section, special abilities are activated by pressing specific buttons:

Jump” button → Activate “Jump, Double Jump, Wall Jump and Glide” → Pressing “Space Bar” (on keyboard) or “Y Button” (on gamepad).

Sprint” button → Activate “Sprint and Wall Run” → By pressing “Left Shift” (on keyboard) or “Left Stick Button” (on gamepad).

Fire2” button → Activate “Dash” → Pressing “Left Alt” (on keyboard) or “Button B” (on gamepad).

Both “Jump” and “Fire2” are already preconfigured in Unity 6. Next we will see how to configure “Sprint“:

First, let’s open Unity’s “Project Settings” (Edit → Project Settings).

In “Project Settings“, click “Input Manager“. Within “Input Manager“, find the “Size” parameter and change the current value to add two more units. That is, if the current value is “30“, change it to “32“. This will create two more items at the end of the list, which will be used to configure “Sprint.”

We will call both elements “Sprint” and each one will have the following configuration:

 

“Ads”

2.3 Final adjustments.

Before testing our game, you need to assign the required components and layers to the “PlayerController” component as we did in the previous tutorial. You also need to add more obstacles of different sizes to allow you to use the new abilities. So, “Get it done!” 😀

By the way, don’t forget to assign the “ground3D” layer to the new obstacles so the game can recognize them as objects on which special abilities are activated.

Yes, the moment has arrived, press play and enjoy your first overcharged 3D game 🙂 ! 

Exercises.

To reinforce what you have learned, it is necessary to practice it, so try doing the following exercises:

  1. Modify values ​​in the “Player Controller” component to make your character move faster, jump higher, and land slower when gliding.
  2. Modify values ​​in the “Cinemachine Orbital Follow” and “Cinemachine Rotation Composer” components found in the “FreeLook Camera” object to experiment with different approaches to your third-person camera.
  3. Create a new scene with many types of obstacles so you can test all the skills like in “The Platformer Games“.
  4. Create more elements within the “Input Manager” to modify the game controls (for example, allowing you to glide using a button other than the jump button).

This Unity Tutorial ends here. We hope you found it helpful and that you’ve created “Your First 3D Game“.

“Ads”

Unity Tutorial: “Your First 3D Game


1. Making a 3D Game

2. Overcharging the Game

See more Tutorials

 

“Ads”

This Tutorial was useful to you?
¡¡Remember, Ads Help Us Maintain this “Great Site” 😀 !!

Share this Post
Posted in First3DGame, Tutorials, Unity.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.