FPS Microgame is a sample project in Unity. Let’s take a look at the scripts used in this project and see how they are set up.

Managers

The GameManager prefab in MainScene contains most of the logic in this project. Attached to this prefab are:

  • <strong>GameFlowManager</strong> – acts like a GameInstance, GameMode, and GameState combined in Unreal. It controls end-of-game behavior: cursor unlock, fade-to-black, audio fade-out, optional victory sound and message, and then scene loading.
  • <strong>EnemyManager</strong> – stores data related to enemies. Each EnemyController registers itself to EnemyManager during Start() and unregisters itself during Die().
  • <strong>ActorsManager</strong> – stores a list of actors in the game. An Actor can be a player or enemy. Actors register themselves to ActorsManager during Start().
  • <strong>ObjectiveManager</strong> – stores a list of Objective in the game. It checks the status of all Objective each frame. When all objectives are completed, call EventManager.Broadcast(Events.AllObjectivesCompletedEvent). By default, this project has no objectives. You can add your own by inheriting from Objective and calling ObjectiveManager.RegisterObjective().
  • <strong>AudioManager</strong> & <strong>AudioUtility</strong> – Instead of having a singleton AudioManager like in other projects, this project has an AudioManager that handles audio mixing, and a separate static class AudioUtility that gets called by GameObjects to play sound. AudioUtility.CreateSFX creates temporary GameObjects with an AudioSource, configures it, plays the clip and lets it self-destruct when finished.

Event System

<strong>EventManager</strong> is a simple pub/sub system that maps a concrete event Type -> a multicast Action and lets code add/remove typed listeners and broadcast event instances to all listeners of that concrete type. The Game Events are stored in Events.cs. I’ll display the code here because this is a good reference for your own event system.

public static class EventManager
{
    static readonly Dictionary<Type, Action<GameEvent>> s_Events = new Dictionary<Type, Action<GameEvent>>();

    static readonly Dictionary<Delegate, Action<GameEvent>> s_EventLookups = new Dictionary<Delegate, Action<GameEvent>>();

    public static void AddListener<T>(Action<T> evt) where T : GameEvent
    {
        if (!s_EventLookups.ContainsKey(evt))
        {
            Action<GameEvent> newAction = (e) => evt((T) e);
            s_EventLookups[evt] = newAction;

            if (s_Events.TryGetValue(typeof(T), out Action<GameEvent> internalAction))
                s_Events[typeof(T)] = internalAction += newAction;
            else
                s_Events[typeof(T)] = newAction;
        }
    }

    public static void RemoveListener<T>(Action<T> evt) where T : GameEvent
    {
        if (s_EventLookups.TryGetValue(evt, out var action))
        {
            if (s_Events.TryGetValue(typeof(T), out var tempAction))
            {
                tempAction -= action;
                if (tempAction == null)
                    s_Events.Remove(typeof(T));
                else
                    s_Events[typeof(T)] = tempAction;
            }

            s_EventLookups.Remove(evt);
        }
    }

    public static void Broadcast(GameEvent evt)
    {
        if (s_Events.TryGetValue(evt.GetType(), out var action))
            action.Invoke(evt);
    }

    public static void Clear()
    {
        s_Events.Clear();
        s_EventLookups.Clear();
    }
}
public static class Events
{
    public static ObjectiveUpdateEvent ObjectiveUpdateEvent = new ObjectiveUpdateEvent();
    public static AllObjectivesCompletedEvent AllObjectivesCompletedEvent = new AllObjectivesCompletedEvent();
    public static GameOverEvent GameOverEvent = new GameOverEvent();
    public static PlayerDeathEvent PlayerDeathEvent = new PlayerDeathEvent();
    public static EnemyKillEvent EnemyKillEvent = new EnemyKillEvent();
    public static PickupEvent PickupEvent = new PickupEvent();
    public static AmmoPickupEvent AmmoPickupEvent = new AmmoPickupEvent();
    public static DamageEvent DamageEvent = new DamageEvent();
    public static DisplayMessageEvent DisplayMessageEvent = new DisplayMessageEvent();
}

public class ObjectiveUpdateEvent : GameEvent
{
    public Objective Objective;
    public string DescriptionText;
    public string CounterText;
    public bool IsComplete;
    public string NotificationText;
}

public class AllObjectivesCompletedEvent : GameEvent { }

public class GameOverEvent : GameEvent
{
    public bool Win;
}

public class PlayerDeathEvent : GameEvent { }

public class EnemyKillEvent : GameEvent
{
    public GameObject Enemy;
    public int RemainingEnemyCount;
}

public class PickupEvent : GameEvent
{
    public GameObject Pickup;
}

public class AmmoPickupEvent : GameEvent
{
    public WeaponController Weapon;
}

public class DamageEvent : GameEvent
{
    public GameObject Sender;
    public float DamageValue;
}

public class DisplayMessageEvent : GameEvent
{
    public string Message;
    public float DelayBeforeDisplay;
}

Player

Player prefab has the following components:

  • <strong>PlayerInputHandler</strong>Start() finds and enables Player actions (Move, Look, Jump, Fire, Aim, Sprint, Crouch, Reload, NextWeapon), locks the cursor, and caches references to GameFlowManager and PlayerCharacterController. LateUpdate tracks whether the fire input was held last frame. It exposes methods that first verify input can be processed, and then return move, look horizontal/vertical, jump, aim, sprint, crouch, reload, fire states, weapon switch and direct weapon selection.
  • <strong>PlayerCharacterController</strong> – reads input from PlayerInputHandler and drives the Unity CharacterController and PlayerCamera to handle movement, jumping, crouching, camera rotation/aiming, slope reorientation, and collision/obstruction handling. It also manages related systems such as audio (footsteps, jump, land, fall-damage), fall-damage and death via Health, interactions with PlayerWeaponsManager and Actor (aim point), kill-height enforcement, and broadcasts stance change events.
  • Health – just a typical health component with UnityAction events OnDamaged, OnHealed, OnDie.
  • <strong>PlayerWeaponsManager</strong> – manages the player’s weapon inventory and runtime behavior. It spawns and assigns weapon prefabs to slots, handles weapon switching with animation, processes input for aiming, firing and reloading, and applies bobbing, recoil and FOV adjustments. It also detects when the weapon is pointing at an enemy, handles adding/removing weapons, and exposes UnityAction events for weapons.
  • <strong>Jetpack</strong> – reads jump input and player state to apply upward acceleration, consuming and refilling a fuel meter over configurable durations and delays, and toggling particle VFX and looping SFX while in use. Requires PlayerCharacterController and PlayerInputHandler to function.
  • <strong>Damageable</strong> – this component does calculations first before calling Health.TakeDamage().

Weapons

The weapons prefab use the following components:

  • <strong>WeaponController</strong> – shooting, ammo management, charging, visual/audio effects, and physical shell ejection.
  • WeaponFuelCellHandler – animates the local positions of a weapon’s fuel-cell GameObjects based on the weapon’s ammo ratio via WeaponController
  • OverheatBehavior – drives the visual and audio feedback for a weapon overheating / cooling based on the weapon’s ammo ratio and cooling state. References WeaponController.
  • <strong>ProjectileStandard</strong> – inherits from ProjectileBase. This is the weapon’s ammo. Hit detection, damage application, VFX, SFX and optional trajectory correction for first‑person aiming. References PlayerWeaponsManager, Damageable, and DamageArea.
  • DamageArea – area-of-effect helper used by projectiles and explosions. Collects colliders in a sphere, finds Damageable and Health component, and applies damage.

Enemies

Enemies can be stationary or mobile. Prefabs have the following components:

  • <strong>EnemyController</strong> – references EnemyManager, WorldspaceHealthBar, ActorsManager, Health, GameflowManager, WeaponController, PatrolPath, DetectionModule, NavigationModule. Orients the enemy and its weapons toward targets, and handles weapon initialization, firing and optional swapping. It responds to damage by flashing body materials, playing a damage sound, notifying the detection module and invoking damage events, and on death it spawns VFX, optionally drops loot, unregisters from the EnemyManager, and destroys the GameObject. The script also updates eye and body material property blocks for combat visuals, enforces level bounds self-destruction, exposes UnityAction events, and draws debug gizmos for detection, attack and path ranges.
  • <strong>EnemyMobile</strong> – it subscribes to an attached EnemyController for events, manages a three-state AI (Patrol, Follow, Attack) with state transitions based on line-of-sight and attack range, updates navigation destinations and orientation, and issues attack attempts.
  • <strong>EnemyTurret</strong> – listens to an EnemyController for events, switches states, smoothly aiming its TurretPivot toward a detected target, waits DetectionFireDelay before firing, and blends back to animation-driven rotation when the target is lost.
  • <strong>DetectionModule</strong> – finds and tracks hostile Actors. It checks all actors from the ActorsManager, uses distance and Physics.RaycastAll to test line-of-sight to each actor’s AimPoint, and records the closest visible hostile. Exposes OnDamaged and OnAttack methods that update the known target and trigger optional animator parameters.

Projectiles

ProjectileStandard inherits from ProjectileBase. It handles launching, moving, collision detection, and impact effects for a fired projectile.

Pickups

Pickups inherit from a base Pickup class and override its OnPicked() method. Checks if interacting object has a PlayerCharacterController and fires an event.

Game HUD

  • JetpackCounter, WeaponHUDManager, PlayerHealthBar, FeedbackFlashHUD, StanceHUD, NotificationHUDManager, Compass, CrosshairManager use FindFirstObjectByType() to get a reference to the component that owns the data they display.
  • Some of the HUD components use Update() instead of events to display information
  • <strong>Compass</strong> works by having <strong>CompassElements</strong> register themselves to the Compass

Editor utilities and tools

  • <strong>DebugUtility</strong> – A static class for handling null errors with GetComponent and FindObject. This class is used throughout the project.
  • <strong>MiniProfiler</strong> – provides performance tips, performs a static scene analysis (counts meshes, skinned meshes, polygon count, non‑combined meshes, rigidbodies, lights, enemies and emits suggestions), and builds a 3D polygon‑count heatmap by subdividing the scene bounds into fixed cells (default size 10) that aggregate triangle counts from mesh and skinned mesh renderers.
  • <strong>MeshCombiner</strong> and <strong>MeshCombineUtility</strong> – works together. Groups renderers into batches by material, submesh index and shadow settings. It also strips ProBuilder components and forces 32‑bit indices for large meshes.
  • <strong>PrefabReplacer</strong> – swaps prefab instances in-scene while supporting undo.

Takeaways

  • This project does not use singletons, service locators, or scriptable objects to communicate between scripts. Classes use FindObject during Start() to get a reference to managers. This is alright for a small project, but should be avoided in production.
  • AudioUtility can be optimized by implementing Object Pooling.

Send me a message.


Recent posts: