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 toEnemyManagerduringStart()and unregisters itself during Die().<strong>ActorsManager</strong>– stores a list of actors in the game. AnActorcan be a player or enemy. Actors register themselves toActorsManagerduring Start().<strong>ObjectiveManager</strong>– stores a list ofObjectivein the game. It checks the status of allObjectiveeach frame. When all objectives are completed, callEventManager.Broadcast(Events.AllObjectivesCompletedEvent). By default, this project has no objectives. You can add your own by inheriting fromObjectiveand callingObjectiveManager.RegisterObjective().<strong>AudioManager</strong>&<strong>AudioUtility</strong>– Instead of having a singletonAudioManagerlike in other projects, this project has anAudioManagerthat handles audio mixing, and a separate static classAudioUtilitythat gets called by GameObjects to play sound.AudioUtility.CreateSFXcreates 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 toGameFlowManagerandPlayerCharacterController.LateUpdatetracks 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 fromPlayerInputHandlerand drives the UnityCharacterControllerand 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 viaHealth, interactions withPlayerWeaponsManagerand Actor (aim point), kill-height enforcement, and broadcasts stance change events.Health– just a typical health component with UnityAction eventsOnDamaged,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. RequiresPlayerCharacterControllerandPlayerInputHandlerto function.<strong>Damageable</strong>– this component does calculations first before callingHealth.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 viaWeaponControllerOverheatBehavior– drives the visual and audio feedback for a weapon overheating / cooling based on the weapon’s ammo ratio and cooling state. ReferencesWeaponController.<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. ReferencesPlayerWeaponsManager,Damageable, andDamageArea.DamageArea– area-of-effect helper used by projectiles and explosions. Collects colliders in a sphere, findsDamageableandHealthcomponent, and applies damage.
Enemies
Enemies can be stationary or mobile. Prefabs have the following components:
<strong>EnemyController</strong>– referencesEnemyManager, 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 anEnemyControllerfor events, switches states, smoothly aiming itsTurretPivottoward a detected target, waitsDetectionFireDelaybefore 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 theActorsManager, uses distance andPhysics.RaycastAllto test line-of-sight to each actor’sAimPoint, and records the closest visible hostile. ExposesOnDamagedandOnAttackmethods 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, CrosshairManageruseFindFirstObjectByType()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 theCompass
Editor utilities and tools
<strong>DebugUtility</strong>– A static class for handling null errors withGetComponentandFindObject. 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
FindObjectduring Start() to get a reference to managers. This is alright for a small project, but should be avoided in production. AudioUtilitycan be optimized by implementing Object Pooling.

