Build Your Own First Person Shooter Game: 8-hour Unity 3D GameDev Tutorial

First-person shooters (FPS) have been a cornerstone of gaming for nearly three decades. From genre-defining classics like Doom and Quake to the modern battle royale phenomenon spearheaded by PUBG and Fortnite, FPS titles consistently push technical and creative boundaries while attracting millions of passionate players.

According to a 2020 report from ICO Partners, FPS was the second most popular genre on Steam, capturing over 20% of total playtime. The PS4 game library was similarly saturated, with FPS titles like Call of Duty and Destiny dominating the best-seller charts. It‘s clear that FPS games have immense market potential, making them an appealing target for both aspiring and veteran game developers.

In this comprehensive tutorial, we‘ll walk through building a complete FPS game prototype from scratch using the Unity 3D engine. By the end of these 8 hours, you‘ll have a solid grasp of essential FPS mechanics and a strong foundation for creating your own action-packed shooters.

Why Choose Unity for FPS Development?

Unity Engine Market Share

Unity has established itself as the go-to game engine for developers worldwide, powering over half of all mobile games and more than 60% of VR/AR experiences. Its user-friendly interface, extensive documentation, and massive asset store make it accessible to developers of all skill levels.

While Unity supports 2D and 3D projects across many genres, it really shines for FPS development. Key features include:

  • Robust first-person controller framework
  • Efficient 3D rendering pipeline with PBR materials and lighting
  • Built-in physics engine with ray and sphere casts
  • Easy integration with platforms like Steam, PlayStation Network, and Xbox Live
  • Cross-platform support for PC, console, mobile, and VR

As a full-stack developer, I‘ve worked with various engines and frameworks, but I keep coming back to Unity for its versatility and performance. Its C#-based scripting is similar to languages like Java and C++, making it approachable for developers from diverse backgrounds.

Anatomy of an FPS: Core Mechanics

Diagram of FPS Game Loop

Before we dive into implementation, let‘s break down the fundamental components found in nearly every first-person shooter:

  1. Player movement (walking, sprinting, crouching, jumping)
  2. Camera control (mouselook, zoom, recoil)
  3. Shooting (hitscan and projectile weapons, bullet physics)
  4. Damage and health (dealing and receiving damage, regeneration)
  5. Inventory management (weapon switching, ammo tracking)
  6. Interactivity (opening doors, picking up items, using ziplines)
  7. AI enemies (patrolling, attacking, taking cover)

The game loop that ties these systems together typically looks like this:

while (true)
{
    ProcessInput();
    UpdateGameState();
    RenderGraphics();
}

Each frame, the engine checks for player input like WASD keys or mouse clicks, updates the positions of game objects and resolves physics, then draws the 3D scene from the player‘s perspective.

To achieve smooth, responsive gameplay, your game should maintain a frame rate of at least 60 FPS (frames per second). This means all your scripts need to execute in under 16.7ms per frame, a performance constraint that requires careful optimization.

Project Setup and Level Creation

Unity Level Editor

We‘ll begin our project by importing a pre-made environment asset into a fresh Unity scene. For this tutorial, we‘re using the Sci-Fi Styled Modular Pack from the Unity Asset Store, but feel free to use your own level geometry.

After importing the level mesh and textures, we need to generate lighting data by baking the scene. This pre-computes light and shadow maps for static geometry, improving rendering speed.

Next, we‘ll create a NavMesh by selecting the walkable surfaces in our level. The NavMesh allows AI-controlled characters to navigate the space intelligently using pathfinding algorithms.

Player Controller

The heart of any FPS is the player controller that translates user input into in-game actions. We‘ll create a new GameObject and attach a CharacterController component to handle movement physics.

To enable WASD movement relative to the camera direction, we can use this script:

public class PlayerController : MonoBehaviour 
{
    public float speed = 5f;

    private CharacterController controller;
    private Transform cameraTransform;

    void Start()
    {
        controller = GetComponent<CharacterController>();
        cameraTransform = Camera.main.transform;
    }

    void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        Vector3 movement = cameraTransform.right * horizontal + 
                           cameraTransform.forward * vertical;
        movement *= speed * Time.deltaTime;

        controller.Move(movement);
    }
}

For crouching and jumping, we can modify the CharacterController‘s height and add vertical velocity, respectively:

// In Update()
if (Input.GetKeyDown(KeyCode.C))
    controller.height = 1f;
else if (Input.GetKeyUp(KeyCode.C))
    controller.height = 2f;

if (Input.GetButtonDown("Jump") && controller.isGrounded)
    verticalVelocity = 5f;

verticalVelocity -= gravity * Time.deltaTime;
movement.y = verticalVelocity * Time.deltaTime;

Camera Control

In Unity, the player‘s view is determined by the position and rotation of the Camera object attached to their avatar. We can control the camera‘s pitch (up/down) and yaw (left/right) using the mouse:

public float mouseSensitivity = 2f;
public float upDownRange = 90f;

private float verticalRotation = 0;
private Transform cameraTransform;

void Start()
{
    cameraTransform = Camera.main.transform;
    Cursor.lockState = CursorLockMode.Locked;
}

void Update() 
{
    float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity;
    float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity;

    verticalRotation -= mouseY;
    verticalRotation = Mathf.Clamp(verticalRotation, -upDownRange, upDownRange);
    cameraTransform.localRotation = Quaternion.Euler(verticalRotation, 0, 0);

    transform.Rotate(Vector3.up * mouseX);
}

This script grabs the mouse delta each frame and applies it to the camera‘s local rotation on the x-axis (for pitch) and the player‘s world rotation on the y-axis (for yaw). We lock the cursor to the game window and clamp the vertical rotation to prevent flipping upside-down.

Weapons and Shooting Mechanics

Weapon Model and Animations

Now for the fun part – letting the player shoot stuff! Most FPS games feature two types of weapons:

  1. Hitscan – Instant line traces that register hits on the first solid surface they encounter (e.g. rifle, laser)
  2. Projectile – Physics-based objects that travel from the muzzle to the target over time (e.g. rocket, grenade)

To implement a basic hitscan weapon, we can perform a raycast from the center of the screen and check if it hits a valid target:

public float damage = 10f;
public float range = 100f;

private Camera fpsCam;

void Start()
{
    fpsCam = Camera.main;
}

void Update()
{
    if (Input.GetButtonDown("Fire1"))
    {
        Vector3 rayOrigin = fpsCam.ViewportToWorldPoint(new Vector3(0.5f, 0.5f, 0));
        RaycastHit hit;

        if (Physics.Raycast(rayOrigin, fpsCam.transform.forward, out hit, range))
        {
            Debug.Log(hit.transform.name);

            Target target = hit.transform.GetComponent<Target>();
            if (target != null)
            {
                target.TakeDamage(damage);
            }
        }
    }
}

When the player presses the fire button, we generate a ray from the camera‘s center to its far clipping plane, then check if that ray intersects any colliders in the scene. If the ray hits an object with the Target script, we call its TakeDamage method to inflict injury.

Projectile weapons work similarly, but instead of performing an immediate raycast, they spawn a new GameObject with a Rigidbody component and initial velocity. The projectile is subject to gravity and can bounce off surfaces before impacting a target.

Here‘s a simple script for launching projectiles:

public GameObject rocketPrefab;
public float rocketSpeed = 10f;

void Update()
{
    if (Input.GetButtonDown("Fire2"))
    {
        GameObject rocketInstance = Instantiate(rocketPrefab, transform.position, transform.rotation);
        Rigidbody rocketRigidbody = rocketInstance.GetComponent<Rigidbody>();
        rocketRigidbody.AddForce(transform.forward * rocketSpeed, ForceMode.VelocityChange);
    }
}

To complete the weapon experience, we‘ll import a 3D gun model and set up animations for actions like shooting, reloading, and switching weapons. By attaching the weapon to a socket on the player‘s hand bone, we can realistically move it as the player aims and fires.

Enemy AI

Enemy Behavior Flowchart

Engaging enemy AI is crucial for making an FPS feel alive and challenging. We‘ll create a basic behavior tree that allows enemies to patrol waypoints until they spot the player, then attack relentlessly.

First, let‘s write a script that makes the enemy rotate towards the player when they enter its field of view:

public float viewAngle = 110f;
public float viewDistance = 10f;

private Transform playerTransform;

void Start () 
{
    playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
}

void Update ()
{
    Vector3 targetDir = playerTransform.position - transform.position;
    float angle = Vector3.Angle(targetDir, transform.forward);

    if (angle <= viewAngle / 2f)
    {
        RaycastHit hit;
        if (Physics.Raycast(transform.position, targetDir.normalized, out hit, viewDistance) && 
            hit.transform == playerTransform)
        {
            RotateTowards(playerTransform.position);
        }
    }
}

Each frame, we calculate the angle between the enemy‘s forward vector and the direction to the player. If that angle is within half of the enemy‘s view cone, we perform a raycast to see if the player is within a visible range. If so, the enemy rotates to face the player.

Next, we can make the enemy chase down and shoot the player when they‘re spotted:

public float moveSpeed = 3f;
public float shootRate = 1f;
public float damage = 5f;

private float shootTimer;

// In Update()
if (angle <= viewAngle / 2f)
{
    // ...
    if (hit.distance > 2f)
    {
        controller.Move(transform.forward * moveSpeed * Time.deltaTime);
    }
    else
    {
        if (Time.time > shootTimer)
        {
            shootTimer = Time.time + shootRate;
            playerTransform.GetComponent<PlayerStats>().TakeDamage(damage);
        }
    }
}

The enemy will chase the player until they‘re within 2 meters, then start shooting on a fixed timer. Each hit deals damage to the PlayerStats script attached to the player character.

To create more interesting enemy encounters, we can define patrol routes using an array of transforms, stagger attacks from multiple enemies, and even implement flanking maneuvers or grenade usage. The key is to create challenging and unpredictable opponent behavior that tests the player‘s mastery of core FPS skills.

Conclusion

Building an FPS in Unity requires a diverse skill set – from game and level design to 3D math and AI programming. But by breaking down the genre into its core components and leveraging Unity‘s powerful features, you can assemble a thrilling shooter experience.

Of course, this tutorial only scratches the surface of modern FPS development. As computing power increases, players expect more from their games: bigger worlds, smarter enemies, and seamless multiplayer modes. Meeting those expectations requires advanced techniques like spatial partitioning, client-server networking, and procedural generation.

If you‘re up for the challenge, there‘s never been a better time to dive into FPS development. With accessible tools, extensive documentation, and supportive communities, anyone can learn to build immersive and memorable shooters. So grab your mouse, warm up your trigger finger, and get ready to create the next great FPS experience!

Similar Posts