NPC System
Overview
The FPS template includes a production-ready NPC system on top of the engine’s base AI classes. While the first entity guide shows how to build a simple enemy from scratch using AIEntityTemplate directly, the template’s BaseNPCController gives you significantly more for free: a perception system, faction-based targeting, squad coordination, a state machine, built-in combat logic, and weapon support. For most enemies you’d build in a full game, you’ll want to extend this rather than AIEntityTemplate.
NPCDefinition
NPCDefinition is a data class that describes an NPC type without any behavior code. Create a subclass of it to define your enemy’s stats, model path, bounding box, and other fixed properties:
public class GruntDefinition : NPCDefinition
{
public override int MaxHP => 5;
public override string ModelPath => “Models/Enemies/grunt.ccmdl”;
public override BoundingBox Bounds => new BoundingBox(
-new Vector3(0.4f, 2.2f, 0.4f), new Vector3(0.4f, 0.15f, 0.4f));
public override float Scale => 1f;
public override float SteerSpeed => 400f;
}
Separating stats into a definition class means you can swap an enemy’s configuration without touching behavior code, and reuse the same controller for variants with different HP and models.
Creating an NPC Entity
Create a WorldEntity subclass that wraps your controller and definition. Pass the definition into the controller constructor:
[EntityDescriptor()]
public class GruntEnemy : WorldEntity
{
public GruntEnemy()
{
var def = new GruntDefinition();
controller = new BaseNPCController(def);
axisAlignedBox = true;
isSimulated = true;
bounds = def.Bounds;
}
}
If you only need to swap stats, you can use BaseNPCController directly with different definitions. If you need custom behavior, subclass BaseNPCController and override what you need.
NPCBehavior and the State Machine
The NPC’s decision-making is handled by an NPCBehavior object. Swap it at any time on a live NPC to completely change how it behaves. The built-in default behavior handles idle wandering, chasing detected threats, and attacking when close enough.
The state machine has these main states:
Idle No active threat. The NPC wanders using GroundNodes.
Alert A threat was detected recently but is no longer in sight. The NPC searches the last known position.
Combat A threat is actively perceived. The NPC chases and attacks.
Access the current state via controller.State. Listen for state changes by overriding OnStateEntered(EnemyState next) in your subclass.
Perception
The perception system determines what the NPC can see and hear. It considers distance, line of sight against the BSP, and the ai_notarget CVar (which disables enemy targeting for debugging). Perceived entities become potential threats that the behavior’s targeting logic ranks and selects from.
The current best threat is available as controller.BestThreat. This updates every frame based on the perception data.
The ai_debug CVar draws perception information in the world viewport. ai_debug_nearest limits the debug draw to only the closest NPC. These are useful when enemies aren’t behaving as expected.
Factions
NPCs belong to a faction that determines who they treat as a threat. The FPS template ships with a FactionManager.Grunts faction defined by default. Enemies of different factions will fight each other; allies share a faction and ignore each other as threats.
// Make this NPC a friendly to the player
controller.Faction = FactionManager.PlayerAllies;
Define your own factions by creating NPCFaction instances and registering them with FactionManager. A faction defines which other factions it considers hostile.
Squads
Multiple NPCs can join a squad and coordinate behavior. Squad members share sighting information: when one member detects a threat, all members become aware of it. Squads also influence movement, keeping members spread out rather than clumping on the same path.
var squad = new NPCSquad();
gruntA.controller.JoinSquad(squad);
gruntB.controller.JoinSquad(squad);
Squads can be created at runtime or set up in a script triggered from the map editor. An NPC can only belong to one squad at a time. Calling JoinSquad on a new squad removes them from their previous one.
Giving NPCs Weapons
NPCs can fire any weapon that inherits from BaseWeapon. The base NPC controller has built-in combat logic that calls TryFirePrimary() when a threat is in range and line of sight. Assign a weapon via the combat setup in your subclass or definition:
// In your BaseNPCController subclass’s OnSpawn:
weapon = new WeaponSMG();
weapon.Owner = entity;
The base controller calls weapon.Update() each frame and handles the aim direction automatically based on the current threat position. See Weapons for details on creating weapon classes.
Using ai_ Console Commands
The FPS template registers several CVars for debugging the AI system:
ai_debug 1 Draws perception cones, paths, and state info for all active NPCs.
ai_debug_nearest 1 Limits debug drawing to the nearest NPC only, to reduce visual noise.
ai_notarget 1 Disables enemy targeting. Enemies still move and react but won’t select the player as a threat.
ai_disable 1 Freezes all NPC update logic. Enemies stop moving and reacting entirely. Useful for screenshot work or testing specific scenarios.