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.
bulletPrefab, firePoint, and the bullet's Rigidbody — previously any unassigned field caused a NullReferenceException on first shotUpdate() and physics to FixedUpdate() — mixing them caused missed inputs at low frame ratesGetButtonDown("Fire1") instead of raw KeyCode so it also responds to gamepad and remappingDebug.LogError when setup is incomplete instead of a silent crashusing 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); } }
offset as a serialized field so students can adjust the camera position from the Inspector without editing codeQuaternion.Slerp to optionally smooth camera rotation (disabled by default — enable by setting rotateWithTarget = true)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 // ─────────────────────────────────────────────────────────────
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 shotlifespan as a backup auto-destroy so bullets don't persist if they miss everythingscoreValue field so individual targets can be worth different pointsusing 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) // ─────────────────────────────────────────────────────────────
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).timeOffset on Start — previously all spawned targets started at the same PingPong phase, so they all moved in perfect synchrony (looked broken)moveAxis option so targets can patrol horizontally, vertically, or bothpointA/pointB are now derived from the spawn position so the prefab works at any spawn locationusing 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 // ─────────────────────────────────────────────────────────────
spawnPoints — previously threw IndexOutOfRangeException if the array was left empty in the Inspector(0, 0.5, 0) when no spawn points are set, so the game still runs during early developmentUnityEngine.UI.Text with TMPro.TextMeshProUGUI — required for Unity 2022+ and Unity 6. Legacy fallback commented inTime.timeScale is now reset to 1 in Awake() — previously if the game was restarted via scene reload after GameOver, time stayed pausedRestartGame() method that properly resets score, lives and timeScale so it can be wired to a UI buttontargetPrefab before spawning to give a clear error instead of a silent crashusing 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 // ─────────────────────────────────────────────────────────────
| 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() |
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.