3D adventures with Unity

Since I found how to start a game instantly in Unity I decided to play more with it. Just in case, even though I played with Unity a few years ago, I wasn’t a professional Unity developer and I can miss something important. I spent a lot of time researching and watching videos and I’m confident in its outcome, but as a human, I can do mistakes too. Keep this in mind when reading the text below.

I decided to create a simple 3D FPS project and see what can I do with it. To my surprise, there is no 3D FPS template in Unity. All it has is an empty 3D project. Ok, no problem, I will find some tutorial, how hard it can be right? It is a basic step in every FPS game and I’m sure that there are many examples and it will be really easy.

It turns out, that it is really hard. I installed the latest version of Unity 2022.1.21f1. I created a floor, placed the capsule, and attached a rigid body. Then I assigned the character controller and when I hit the Play button, my capsule jumped like 5 meters high.

As I found out later, rigid body and character controller are incompatible together. Some videos are using a rigid body for their examples and some are using a character controller. Very confusing for beginners. Ok, I removed the rigid body because the rigid body is necessary for the physics game when forces apply to your object. For the FPS game style, the character controller is the best choice. A capsule collider is not necessary as well.

Then I was trying to set up controls. After watching a few relatively fresh videos I found that it is normal to check the status key in the Update method. It looks very strange to me as I get used to the abstraction of Input in Unreal Engine. I found that there is something similar in Unity (New Input System) but it takes a lot of time to set up. I honestly do not understand why this system appears only a couple of years ago.

Anyway, I did set up input actions, and I am finally ready to implement movement, mouse look, and jump actions. And then I found out that gravity has to be handled manually. Found code and incorporated it into my game.

Then I found that jump does not work as expected. It turns out that if there is zero vertical movement then the character controller does not check for collision and as result characterController.isGrounded will return false. To fix it, I had to apply minimal downward movement.

After spending a whole day watching videos, reading documentation, and forums I was finally able to craft a simple game where the player can look around, walk, jump, and gravity is working correctly. I would like to state again, that I had previous Unity experience and I have about one year of Unreal Engine experience. And lastly, I have tens of years of software development experience. And it took me one day to build an empty 3D FPS game.

It clearly should be much much simpler than that. Let’s compare how long will it take to do the same in Unreal Engine. All you need is to select a “First Person” template and press Create button and your game is ready. You can look around, walk, jump, pick up a gun, and shoot. There are complex structures around, so you can test the gravity and jumps. There are even boxes to shoot. It is way easier to start for the beginner.

Anyway, here is the basic code of my script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using static UnityEngine.InputSystem.InputAction;

public class PlayerMovement : MonoBehaviour
{
    private const float gravity = 9.81f;
    private const float walkSpeed = 3f;
    private const float jumpHeight = 0.85f;
    private const int mouseSensitivityX = 20;
    private const int mouseSensitivityY = 20;
    private const float mouseMinAngle = -85f;
    private const float mouseMaxAngle = 85;

    private CharacterController characterController;
    private Vector3 verticalVelocity;
    private Vector2 moveVector;
    private Vector2 lookVector;
    private bool jumpPressed;

    public Transform playerCamera;

    private void Awake()
    {
        characterController = GetComponent<CharacterController>();
    }

    void Update()
    {
        // Handle gravity
        if (characterController.isGrounded)
        {
            verticalVelocity.y = -1; // To force recalculate collision or else characterController.isGrounded will be false
        }
        else
        {
            verticalVelocity.y += -gravity * Time.deltaTime;
        }

        // Handle jump
        if (jumpPressed)
        {
            if (characterController.isGrounded)
            {
                verticalVelocity.y = Mathf.Sqrt(2f * jumpHeight * gravity);
            }

            jumpPressed = false;
        }

        // Movement plus gravity
        Vector3 horizonalVelocity = (transform.right * moveVector.x + transform.forward * moveVector.y) * walkSpeed;

        characterController.Move(horizonalVelocity * Time.deltaTime + verticalVelocity * Time.deltaTime);

        // Look up/down
        Vector3 targetRotation = playerCamera.eulerAngles;
        targetRotation.x -= Mathf.Clamp(lookVector.y * Time.deltaTime * mouseSensitivityY, mouseMinAngle, mouseMaxAngle);
        playerCamera.eulerAngles = targetRotation;

        // Look left/right
        targetRotation = transform.eulerAngles;
        targetRotation.y += lookVector.x * Time.deltaTime * mouseSensitivityX;
        transform.eulerAngles = targetRotation;
    }

    public void DoMovement(CallbackContext context)
    {
        moveVector = context.ReadValue<Vector2>();
    }

    public void DoLook(CallbackContext context)
    {
        lookVector = context.ReadValue<Vector2>();
    }

    public void DoJump(CallbackContext context)
    {
        jumpPressed = context.performed;
    }
}

I hope it helps someone.