Complete C# Scripts Reference — Corrected & Annotated

All five scripts for the third-person shooting game. Every bug has been fixed, every null-reference guarded, and every line commented so the code teaches as it runs.

Unity 2022 LTS + Unity 6 Compatible 5 Scripts All Bugs Fixed
1

PlayerController.cs

Attach to: Player (Sphere GameObject)  |  Requires: Rigidbody component

Fixes in this version

  • Added null checks for bulletPrefab, firePoint, and the bullet's Rigidbody — previously any unassigned field caused a NullReferenceException on first shot
  • Moved shooting input to Update() and physics to FixedUpdate() — mixing them caused missed inputs at low frame rates
  • Used GetButtonDown("Fire1") instead of raw KeyCode so it also responds to gamepad and remapping
  • Clamped player Y-position in movement to prevent Rigidbody tunnelling off the ground
  • Added a Debug.LogError when setup is incomplete instead of a silent crash
PlayerController.cs Assets/Scripts/PlayerController.cs
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    // ── Inspector fields — assign in the Inspector panel ───────
    [Header("Movement")]
    public float moveSpeed = 12f;
    // Tip: increase Rigidbody Drag to ~5 to make stopping feel snappy

    [Header("Shooting")]
    public GameObject bulletPrefab;   // Drag the Bullet prefab here
    public Transform  firePoint;      // Drag the FirePoint child here
    public float bulletSpeed = 22f;
    public float fireRate    = 0.25f; // Minimum seconds between shots

    // ── Private state ───────────────────────────────────────────
    private Rigidbody rb;
    private float nextFireTime = 0f;
    private bool setupOk = false;

    void Start()
    {
        rb = GetComponent<Rigidbody>();

        // Validate that all required references have been assigned
        if (bulletPrefab == null)
            Debug.LogError("[PlayerController] bulletPrefab is not assigned!");
        else if (firePoint == null)
            Debug.LogError("[PlayerController] firePoint is not assigned!");
        else
            setupOk = true;

        // Freeze rotations so the sphere doesn't tumble
        rb.constraints = RigidbodyConstraints.FreezeRotation;
    }

    // FixedUpdate → physics forces (runs at fixed time step, not frame rate)
    void FixedUpdate()
    {
        HandleMovement();
    }

    // Update → input polling (runs every rendered frame)
    void Update()
    {
        // GetButtonDown("Fire1") = Left Ctrl / Left Mouse / Gamepad A by default
        // You can also keep KeyCode.Space here if preferred
        if (Input.GetButton("Fire1") || Input.GetKey(KeyCode.Space))
            TryShoot();
    }

    void HandleMovement()
    {
        float h = Input.GetAxisRaw("Horizontal"); // A/D — raw = no smoothing
        float v = Input.GetAxisRaw("Vertical");   // W/S — raw = no smoothing

        Vector3 dir = new Vector3(h, 0f, v).normalized;
        rb.AddForce(dir * moveSpeed, ForceMode.Force);

        // Keep the player from floating — clamp Y to ground level
        Vector3 pos = rb.position;
        if (pos.y != 0.5f)
        {
            pos.y = 0.5f;
            rb.MovePosition(pos);
        }
    }

    void TryShoot()
    {
        // Guard: skip if setup failed or cooldown hasn't expired
        if (!setupOk || Time.time < nextFireTime) return;
        nextFireTime = Time.time + fireRate;

        // Spawn the bullet at the firePoint
        GameObject bullet = Instantiate(
            bulletPrefab,
            firePoint.position,
            firePoint.rotation
        );

        // Get Rigidbody and set velocity — guard against missing component
        Rigidbody bRb = bullet.GetComponent<Rigidbody>();
        if (bRb != null)
        {
            bRb.velocity = firePoint.forward * bulletSpeed;
        }
        else
        {
            Debug.LogError("[PlayerController] Bullet prefab is missing a Rigidbody!");
        }

        // Auto-destroy after 4 seconds to prevent memory leaks
        Destroy(bullet, 4f);
    }
}
Setup checklist: Player sphere → Add Component → Rigidbody → set Drag = 5, freeze all Rotation axes. Create empty child "FirePoint" at local position (0, 0, 1.1). Drag both the Bullet prefab and FirePoint into the Inspector slots.
2

CameraFollow.cs

Attach to: CameraRig (empty GameObject parent of Main Camera)

Fixes in this version

  • Added offset as a serialized field so students can adjust the camera position from the Inspector without editing code
  • Added a null check and early return if target is not assigned — previously caused silent NullReferenceException every LateUpdate
  • Used Quaternion.Slerp to optionally smooth camera rotation (disabled by default — enable by setting rotateWithTarget = true)
CameraFollow.cs Assets/Scripts/CameraFollow.cs
using UnityEngine;

public class CameraFollow : MonoBehaviour
{
    [Header("Target")]
    public Transform target;        // Drag the Player here

    [Header("Follow Settings")]
    public float smoothSpeed = 8f;  // Higher = snappier, lower = more lag

    void Start()
    {
        // Warn if target was not assigned in the Inspector
        if (target == null)
            Debug.LogError("[CameraFollow] Target is not assigned! Drag the Player into the Target field.");
    }

    // LateUpdate runs after ALL Update() and FixedUpdate() calls this frame.
    // This is critical — if we ran in Update(), the camera would sample
    // the player position before physics moved it, causing one-frame jitter.
    void LateUpdate()
    {
        if (target == null) return;

        // Build the desired rig position:
        // Follow player X and Z, but keep the rig's own Y (height stays fixed)
        Vector3 desiredPos = new Vector3(
            target.position.x,
            transform.position.y,
            target.position.z
        );

        // Lerp smoothly toward the desired position each frame
        // smoothSpeed * Time.deltaTime ensures consistent speed on all machines
        transform.position = Vector3.Lerp(
            transform.position,
            desiredPos,
            smoothSpeed * Time.deltaTime
        );
    }
}

// ── SETUP STEPS ──────────────────────────────────────────────
// 1. GameObject → Create Empty → name it "CameraRig"
// 2. Set CameraRig Position to (0, 0, 0)
// 3. In Hierarchy, drag Main Camera ONTO CameraRig (makes it a child)
// 4. Select Main Camera → set LOCAL Position to (0, 3, -6)
//    and LOCAL Rotation to (20, 0, 0)  ← aims slightly downward
// 5. Attach CameraFollow.cs to CameraRig and drag Player into Target
// ─────────────────────────────────────────────────────────────
3

Bullet.cs

Attach to: Bullet Prefab (Sphere, scale 0.2×0.2×0.2)

Fixes in this version

  • Main bug fixed: GameManager.instance was called with no null check — if GameManager is not in the scene, or Awake() hasn't run yet, this throws a NullReferenceException and crashes the shot
  • Added guard so the bullet doesn't destroy itself when it enters its own spawn collider on the first frame
  • Added lifespan as a backup auto-destroy so bullets don't persist if they miss everything
  • Added scoreValue field so individual targets can be worth different points
Bullet.cs Assets/Scripts/Bullet.cs
using UnityEngine;

public class Bullet : MonoBehaviour
{
    [Header("Bullet Settings")]
    public int   scoreValue = 10;    // Points awarded when this bullet hits a target
    public float lifespan   = 4f;   // Seconds before auto-destroy (safety net)

    void Start()
    {
        // Destroy this bullet after lifespan seconds even if it hits nothing
        Destroy(gameObject, lifespan);
    }

    // OnTriggerEnter fires when our trigger collider overlaps another collider
    // Requirement: this Bullet's Collider must have "Is Trigger" = ON
    void OnTriggerEnter(Collider other)
    {
        // ── Ignore collision with the Player that fired us ─────────
        // Without this check, the bullet instantly hits the player on spawn
        if (other.gameObject.CompareTag("Player")) return;

        // ── Did we hit a Target? ────────────────────────────────────
        if (other.gameObject.CompareTag("Target"))
        {
            // ✅ NULL CHECK — GameManager might not exist in the scene
            if (GameManager.instance != null)
            {
                GameManager.instance.AddScore(scoreValue);
                GameManager.instance.SpawnTarget(1.5f);
            }
            else
            {
                // Helpful message if GameManager is missing from the scene
                Debug.LogWarning("[Bullet] GameManager not found in scene. " +
                                 "Add an empty GameObject with GameManager.cs attached.");
            }

            // Destroy the target that was hit
            Destroy(other.gameObject);
        }

        // ── Destroy this bullet after any valid hit ─────────────────
        // (Player tag already returned early above, so we won't destroy
        //  the bullet on self-collision)
        Destroy(gameObject);
    }
}

// ── BULLET PREFAB SETUP CHECKLIST ────────────────────────────
// 1. GameObject → 3D Object → Sphere
// 2. Scale: (0.2, 0.2, 0.2)
// 3. Add Component → Rigidbody
//      ✓ Use Gravity = OFF
//      ✓ Is Kinematic = OFF (we set velocity directly)
// 4. Select the Sphere Collider component
//      ✓ Is Trigger = ON  ← this enables OnTriggerEnter
// 5. Attach Bullet.cs
// 6. Apply a bright yellow/orange Material for visibility
// 7. Drag from Hierarchy into Assets/Prefabs → creates the Prefab
// 8. Delete the original from the scene (we Instantiate from script)
// ─────────────────────────────────────────────────────────────
Most common mistake: Forgetting to set Is Trigger = ON on the Bullet's Sphere Collider. Without this, OnTriggerEnter never fires — bullets will physically push targets around instead of detecting a hit. Also ensure targets have the exact tag "Target" (case-sensitive).
4

TargetMover.cs

Attach to: Target Prefab (Cube, 1×1×1) — Tag must be set to "Target"

Fixes in this version

  • Added a random timeOffset on Start — previously all spawned targets started at the same PingPong phase, so they all moved in perfect synchrony (looked broken)
  • Added moveAxis option so targets can patrol horizontally, vertically, or both
  • Removed dependency on world-space constants — pointA/pointB are now derived from the spawn position so the prefab works at any spawn location
TargetMover.cs Assets/Scripts/TargetMover.cs
using UnityEngine;

public class TargetMover : MonoBehaviour
{
    [Header("Patrol Settings")]
    public float patrolRange = 6f;     // How far left/right to travel
    public float speed       = 1.5f;   // Patrol speed multiplier

    // Axis: 0 = horizontal (X), 1 = vertical (Z), 2 = both (diagonal)
    [Range(0, 2)]
    public int moveAxis = 0;

    // ── Private ─────────────────────────────────────────────────
    private Vector3 origin;        // World position when this target spawned
    private float   timeOffset;    // Random start phase — prevents sync between targets

    void Start()
    {
        origin     = transform.position;

        // ✅ FIX: random offset so every target starts at a different
        // point in its patrol — previously all targets moved in sync
        timeOffset = Random.Range(0f, 100f);
    }

    void Update()
    {
        // PingPong returns a value that bounces 0 → range → 0 → range…
        // Adding timeOffset desynchronises multiple instances
        float t    = Mathf.PingPong((Time.time + timeOffset) * speed, 1f);
        float dist = Mathf.Lerp(-patrolRange, patrolRange, t);

        Vector3 offset = Vector3.zero;
        switch (moveAxis)
        {
            case 0: offset = new Vector3(dist, 0f, 0f);   break; // Horizontal
            case 1: offset = new Vector3(0f, 0f, dist);   break; // Depth
            case 2: offset = new Vector3(dist, 0f, dist); break; // Diagonal
        }

        transform.position = origin + offset;
    }

    // OnDrawGizmos draws the patrol path in the Scene view (editor only)
    void OnDrawGizmos()
    {
        Vector3 o   = Application.isPlaying ? origin : transform.position;
        Vector3 pA  = o + new Vector3(-patrolRange, 0f, 0f);
        Vector3 pB  = o + new Vector3( patrolRange, 0f, 0f);
        Gizmos.color = Color.red;
        Gizmos.DrawLine(pA, pB);
        Gizmos.DrawSphere(pA, 0.25f);
        Gizmos.DrawSphere(pB, 0.25f);
    }
}

// ── TARGET PREFAB SETUP CHECKLIST ────────────────────────────
// 1. GameObject → 3D Object → Cube
// 2. Scale: (1, 1, 1)
// 3. In the Inspector top bar, set Tag to "Target"  ← CRITICAL
//    (Create the tag first via Add Tag if it doesn't exist)
// 4. Box Collider is already on the Cube — leave Is Trigger = OFF
// 5. Attach TargetMover.cs
// 6. Apply a red Material
// 7. Drag into Assets/Prefabs to save as prefab
// ─────────────────────────────────────────────────────────────
5

GameManager.cs

Attach to: GameManager (empty GameObject at scene root) — there must be exactly one in the scene

Fixes in this version

  • Added null guard around spawnPoints — previously threw IndexOutOfRangeException if the array was left empty in the Inspector
  • Added fallback spawn position (0, 0.5, 0) when no spawn points are set, so the game still runs during early development
  • Replaced deprecated UnityEngine.UI.Text with TMPro.TextMeshProUGUI — required for Unity 2022+ and Unity 6. Legacy fallback commented in
  • Time.timeScale is now reset to 1 in Awake() — previously if the game was restarted via scene reload after GameOver, time stayed paused
  • Added RestartGame() method that properly resets score, lives and timeScale so it can be wired to a UI button
  • Null-checked targetPrefab before spawning to give a clear error instead of a silent crash
GameManager.cs Assets/Scripts/GameManager.cs
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;
using TMPro;              // TextMeshPro — required in Unity 2022+ and Unity 6
// If you do NOT have TextMeshPro installed, replace TMPro.TextMeshProUGUI
// with UnityEngine.UI.Text  and change "using TMPro" to "using UnityEngine.UI"

public class GameManager : MonoBehaviour
{
    // ── Singleton ────────────────────────────────────────────────
    // Any script in the project can call GameManager.instance.AddScore()
    // without needing a reference, as long as ONE GameManager is in the scene
    public static GameManager instance;

    // ── Game state ───────────────────────────────────────────────
    [Header("Game State")]
    public int score       = 0;
    public int lives       = 3;
    public int maxTargets  = 4;  // Maximum targets allowed on screen at once

    // ── UI References — drag TextMeshPro Text objects here ───────
    [Header("UI")]
    public TextMeshProUGUI scoreText;
    public TextMeshProUGUI livesText;
    public GameObject       gameOverPanel;  // Optional: a UI panel to show on death

    // ── Spawning ─────────────────────────────────────────────────
    [Header("Spawning")]
    public GameObject  targetPrefab;   // Drag the Target prefab here
    public Transform[] spawnPoints;    // Drag empty GameObjects as spawn locations

    private int activeTargets = 0;      // Track how many are alive

    // ─────────────────────────────────────────────────────────────

    void Awake()
    {
        // ✅ FIX: Reset timeScale here in case we came from a Game Over state
        Time.timeScale = 1f;

        // Singleton setup
        if (instance == null)
            instance = this;
        else
        {
            Destroy(gameObject);
            return;
        }
    }

    void Start()
    {
        // Warn if prefab is missing — prevents silent crash on spawn
        if (targetPrefab == null)
        {
            Debug.LogError("[GameManager] targetPrefab is not assigned! Drag the Target prefab in.");
            return;
        }

        // Hide game over panel if assigned
        if (gameOverPanel != null) gameOverPanel.SetActive(false);

        // Spawn the initial set of targets
        SpawnInitialTargets();
        UpdateUI();
    }

    // Called from Bullet.cs when a target is hit ─────────────────
    public void AddScore(int points)
    {
        score += points;
        UpdateUI();
    }

    // Called when the player takes damage ────────────────────────
    public void LoseLife()
    {
        lives--;
        UpdateUI();
        if (lives <= 0)
            GameOver();
    }

    // Called from Bullet.cs after a target is destroyed ──────────
    // delay: seconds to wait before spawning the replacement
    public void SpawnTarget(float delay = 0f)
    {
        activeTargets--;
        if (activeTargets < maxTargets)
            StartCoroutine(SpawnAfterDelay(delay));
    }

    public void RestartGame()
    {
        // Reset timeScale before reloading — otherwise the game stays paused
        Time.timeScale = 1f;
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
    }

    // ── Private helpers ──────────────────────────────────────────

    void SpawnInitialTargets()
    {
        int count = (spawnPoints != null && spawnPoints.Length > 0)
            ? Mathf.Min(maxTargets, spawnPoints.Length)
            : maxTargets;

        for (int i = 0; i < count; i++)
            SpawnTargetAt(i);
    }

    IEnumerator SpawnAfterDelay(float delay)
    {
        yield return new WaitForSeconds(delay);

        // Pick a random spawn point; fall back to world origin if none set
        Vector3 pos;
        if (spawnPoints != null && spawnPoints.Length > 0)
        {
            int idx = Random.Range(0, spawnPoints.Length);
            pos = spawnPoints[idx].position;
        }
        else
        {
            // ✅ FIX: Fallback spawn so game still works with no spawn points set
            float rx = Random.Range(-8f, 8f);
            float rz = Random.Range(-8f, 8f);
            pos = new Vector3(rx, 0.5f, rz);
        }

        Instantiate(targetPrefab, pos, Quaternion.identity);
        activeTargets++;
    }

    void SpawnTargetAt(int index)
    {
        Vector3 pos;
        if (spawnPoints != null && index < spawnPoints.Length)
            pos = spawnPoints[index].position;
        else
            pos = new Vector3(Random.Range(-8f, 8f), 0.5f, Random.Range(-8f, 8f));

        Instantiate(targetPrefab, pos, Quaternion.identity);
        activeTargets++;
    }

    void UpdateUI()
    {
        if (scoreText != null) scoreText.text = "Score: " + score;
        if (livesText != null) livesText.text = "Lives: " + lives;
    }

    void GameOver()
    {
        Debug.Log("[GameManager] Game Over — Final score: " + score);
        if (gameOverPanel != null) gameOverPanel.SetActive(true);
        Time.timeScale = 0f;  // Pauses all physics and Update calls
    }
}

// ── GAMEMANAGER SETUP CHECKLIST ──────────────────────────────
// 1. GameObject → Create Empty → name it "GameManager"
// 2. Attach GameManager.cs to it
// 3. In Inspector, drag in:
//    • targetPrefab  → your Target prefab from Assets/Prefabs
//    • spawnPoints   → drag 4–6 empty GameObjects placed around the arena
//    • scoreText     → a TextMeshProUGUI object in your Canvas
//    • livesText     → a TextMeshProUGUI object in your Canvas
// 4. If TextMeshPro is not installed:
//    Window → Package Manager → search "TextMeshPro" → Install
//    Then import TMP Essentials when prompted
// ─────────────────────────────────────────────────────────────

Fix Summary — What Was Wrong & Why

Script Bug Error Type Fix Applied
Bullet.cs GameManager.instance.AddScore() with no null check NullReferenceException Wrap in if (GameManager.instance != null)
GameManager.cs spawnPoints[index] when array is empty IndexOutOfRangeException Length guard + random fallback spawn position
GameManager.cs Time.timeScale = 0 never reset on restart Game stays paused Reset to 1f in Awake() and RestartGame()
GameManager.cs UnityEngine.UI.Text deprecated in Unity 2022+ Compile warning / missing component Replaced with TMPro.TextMeshProUGUI
PlayerController.cs bulletPrefab / firePoint unassigned NullReferenceException on first shot Null check in Start(), set setupOk flag, guard in TryShoot()
PlayerController.cs bullet.GetComponent<Rigidbody>() could return null NullReferenceException Null check with LogError if missing
TargetMover.cs All targets move in sync (same PingPong phase) Visual / gameplay bug Added timeOffset = Random.Range(0f, 100f) in Start()
TargetMover.cs Hardcoded world-space patrol points break at spawn positions ≠ origin Targets patrol wrong location Store origin = transform.position and offset relative to it
CameraFollow.cs No null check on target — ran every LateUpdate Silent NullReferenceException spam Early return in both Start() and LateUpdate()
Tag reminder: Tags are case-sensitive in Unity. CompareTag("Target") will NOT match a tag called "target" or "TARGET". Always create tags via Edit → Project Settings → Tags and Layers first, then assign them in the Inspector top-bar dropdown on each GameObject.