NPC System

Overview

The FPS template’s NPC system is built around an abstract NpcController class. Inherit from it to create your own controller for your NPC. The required functions are: CreateDefinition(), which returns an NpcDefinition data object describing your NPC’s stats, and OnSetup(), which registers your state machine states using DefineState(). The base class handles physics, pathfinding, perception, animation, barks, squad coordination, and save/restore; your subclass only needs to describe behavior.

The template ships with two concrete examples: GruntController (a hostile enemy with suppression/advance squad tactics and grenade-throwing) and CompanionController (a player-following ally). Both are good references when building your own NPCs, though I have no doubt that you’ll write better AI code than my boilerplate stuff.

NpcDefinition

NpcDefinition is basically just the data component for an NPC.

protected override NpcDefinition CreateDefinition() => new NpcDefinition
{
    MaxHP = 48,
    ModelPath = “Models/Enemies/grunt.ccmdl”,
    RunSpeed = 5f,
    OptimalCombatRange = 10f,
    SightRange = 28f,
    SightFOVDegrees = 150f,
    AttackRange = 25f,
};

The full set of available fields:

ModelPath / Scale

Path to the .ccmdl file, and a uniform scale multiplier.

Bounds

The NPC’s collision box. Defaults to a standing-height humanoid.

MaxHP

Starting and maximum hit points.

WalkSpeed / RunSpeed / CreepSpeed

Speeds in world units per second. Used by SetSpeed(SpeedPreset).

MoveAccel / SteerSpeed / GroundFriction

Movement responsiveness. SteerSpeed controls how fast the NPC rotates toward its movement direction.

SightRange / SightFOVDegrees

Line-of-sight cone. The NPC will only see entities within this range and angle.

HearRange / PerceptionTickRate

Radius for hearing moving entities. Perception runs on a timer rather than every frame; PerceptionTickRate sets the interval in seconds.

AttackRange / OptimalCombatRange

The range at which the NPC will fire, and the distance it tries to maintain during combat maneuvering.

ReactionTime / LostTargetTimeout

Seconds before transitioning from alert to full combat, and how long the NPC keeps searching after losing sight of a threat.

SearchDuration / InvestigateDuration

How long the NPC searches the last known position before giving up, and how long it investigates a heard sound.

Animations

An NpcAnimationConfig instance mapping movement/combat states to animation sequence names. See below.

NpcAnimationConfig

NpcDefinition.Animations is an NpcAnimationConfig object whose string fields map to sequence names in your model. The defaults match the template’s placeholder model; override whichever names differ in your own model:

Animations = new NpcAnimationConfig
{
    Idle = “idle”,
    WalkForward = “walkf”,
    RunForward = “runf”,
    RunBack = “runb”,
    RunLeft = “runl”,
    RunRight = “runr”,
    WeaponReady = “smg_ready”,
    LookLeft = “look_left”,
    LookRight = “look_right”,
    Flinch = “flinch_1”,
}

The animation system blends these automatically. You don’t need to drive them manually unless you’re building something outside the locomotion/combat pattern.

Creating an NPC Entity

Create a WorldEntity subclass that instantiates your controller, sets its faction and squad, and equips its weapons. Do this all in the constructor before the entity is spawned:

[EntityDescriptor]
public class GruntEnemy : WorldEntity
{
    static readonly BoundingBox StandingBounds = new BoundingBox(
        -new Vector3(0.25f, 2.0f, 0.25f), new Vector3(0.25f, 0.15f, 0.25f));


    public NpcController NpcController => (NpcController)Controller;


    public GruntEnemy()
    {
        Controller = new GruntController();


        var npc = NpcController;
        npc.Faction = FactionManager.Grunts;
        npc.JoinSquad(NpcSquads.GruntSquad);
        npc.AddWeapon(new WeaponSMG());


        Bounds = StandingBounds;
        AxisAlignedBox = true;
        IsSimulated = true;
    }
}

Note

Weapons are added via AddWeapon() on the controller, not via direct assignment. The controller maintains an inventory list and a currently equipped weapon. AddWeapon() automatically equips the first weapon added.

Subclassing NpcController

Your controller subclass must implement two abstract members:

CreateDefinition()

Returns a configured NpcDefinition. Called once during OnSpawn, before OnSetup.

OnSetup()

Called after the model, physics, and weapons are ready. Register your states and bark lines here.

You can also override InitialState to change which state the NPC starts in (defaults to “idle”), and virtual methods OnDamaged(), OnKilled(), OnSignal(), ShouldTrackEntity(), and GetEntityNoiseLevel() to customize reactions.

The State Machine

States are registered in OnSetup() using DefineState(). The tick delegate runs every frame and returns the name of the state to transition to, or null to remain in the current state. Transitions fire the outgoing state’s exit action and the incoming state’s enter action:

DefineState(“idle”,
    enter: () =>
    {
        SetSpeed(SpeedPreset.Walk);
        StopMoving();
    },
    tick: dt =>
    {
        if (BestThreat != null && CanSee(BestThreat)) return “alert”;
        if (HeardSomething) return “investigate”;
        if (ShouldRewander()) MoveTo(PickWanderPoint());
        return null;
    });

Pass isCombatState: true to any state that represents active combat, this sets IsInCombat automatically, which affects squad awareness. To trigger a transition from outside a tick (e.g. from a signal handler), call GoTo(“statename”).

The current state name is available as controller.CurrentState. State names are arbitrary strings you choose when defining them.

Perception

Perception runs automatically on a timer based on PerceptionTickRate.

BestThreat

The highest-priority currently perceived threat, updated every frame. null if the NPC is unaware of any threats.

Threats

The full list of PerceptionEntry objects. Each entry has Entity, ThreatLevel (0–1), LastKnownPosition, LastSeenTime, IsCurrentlyVisible, and IsRecent.

HeardSomething / HeardPosition

Set by the perception system when a moving entity is within hear range but not in sight. Check in your idle/investigate states to trigger an investigation.

LastKnownTargetPosition

The last world position where the NPC had eyes on its best threat. Valid even after losing sight; used by the search sweep logic.

You can also query perception manually:

if (CanSee(someEntity)) { … }
if (CanHear(somePosition, intensityScale: 2f)) { … }
RegisterThreat(someEntity, threatBoost: 0.8f);

RegisterThreat() is how you make an NPC instantly aware of something — for example when hit by damage, or when scripted to notice the player. Boost values above 0.3f propagate to squad members.

Override ShouldTrackEntity(entity) to control which entities the perception system considers at all, and GetEntityNoiseLevel(entity) to return how loud a given entity is (the default returns 1 if the entity is moving, 0 if still).

Movement and Steering

All movement is driven by calls from your state machine tick. The base class handles pathfinding and physics internally.

MoveTo(point)

Path to a world position. Recalculates the path if the destination changes by more than 0.5 units.

Follow(entity)

Continuously re-paths to a moving entity. Automatically updates the path as the target moves.

StopMoving()

Halts movement intent. The NPC decelerates and stops.

IsPathCompleted()

Returns true when the current path has been fully walked. Use this to know when to pick the next wander point or advance a sweep.

SetSpeed(SpeedPreset)

Sets the speed multiplier to Creep, Walk, or Run, relative to the definition’s RunSpeed.

FaceBodyToward(worldPos) / FaceMovementDirection()

Overrides the NPC’s look angle to face a point in world space, or releases the override so it naturally faces its movement direction.

AddLookTarget(target, priority)

Adds a world-space look target for animation this frame. Higher priority wins if multiple are added. Cleared each frame. Used to make the NPC’s upper body turn toward a threat while the legs move elsewhere.

AddInterest(direction, weight) / AddDanger(direction, weight)

Steering votes. Interest pushes the NPC toward a direction; danger pushes it away. Votes are composited with the current path direction each frame and then cleared. Useful for obstacle avoidance or flocking.

Wander helpers are also provided. PickWanderPoint() scores a set of random candidates based on distance from a home position, visit recency (tracked automatically), and distance from the home, then returns the best one. ShouldRewander() returns true when the current path is complete or a timeout has elapsed. Override WanderHomeRadius, WanderRevisitCooldown, and WanderTimeout to tune the behavior.

Combat

AddWeapon(weapon) / EquipWeapon(index)

Adds a weapon to the NPC’s inventory. The first weapon is auto-equipped. Call EquipWeapon() to switch during gameplay.

FireAt(entity) / FireAt(worldPos)

Fires the current weapon toward a target. Respects SetCanFire(false). Handles aim direction automatically.

ThrowGrenade(targetPos)

Calculates a launch velocity toward the target, leading for flight time, and spawns a GrenadeEntity. Returns false if the throw is out of range or the required velocity would be unrealistically large.

CurrentWeapon / Inventory

Read-only access to the currently equipped weapon and the full inventory list.

The base class automatically reloads when a weapon runs dry. You don’t need to manage weapon.Reload() yourself unless you want explicit control over when reloads happen.

Control Toggles

Three one-line toggles let you override the NPC’s capability from scripts or behavior logic:

controller.SetCanMove(false); // freeze movement, AI still ticks
controller.SetCanFire(false); // prevent all weapon fire
controller.SetInvincible(true); // ignore all incoming damage

Barks

Barks are voiced lines that play in response to game events. Register lines in OnSetup() and fire them by category.

// In OnSetup():
AddBarkLine(“contact”, “Contact!”, “Audio/Barks/contact_01.ogg”);
AddBarkLine(“contact”, “I see them!”, “Audio/Barks/contact_02.ogg”);
AddBarkLine(“pain”, “I’m hit!”, “Audio/Barks/pain_01.ogg”);
AddBarkLine(“suppressing”,“Suppressing!”, null); // no audio, text caption only


// Elsewhere:
Bark(“contact”);

Multiple lines per category are selected by weighted random. The optional weight parameter (default 1.0) biases selection toward or away from a line. When a bark fires and has no audio associated, it sets a caption above the NPC’s head for BarkCaptionDuration seconds.

Bark cooldown tuning fields (set on the controller after construction): BarkGlobalCooldown (minimum seconds between any two barks from this NPC), BarkCategoryCooldown (minimum seconds between barks of the same category), BarkCaptionDuration (how long the caption stays visible).

You can also fire a bark externally using controller.Signal(“bark_contact”), any signal whose name starts with bark_ routes to Bark() automatically.

Signals

Signals are a lightweight string message system for inter-NPC or script-to-NPC communication. Call controller.Signal(“signal_name”, optionalData) from anywhere; the controller’s OnSignal() virtual is invoked. Squads can broadcast a signal to all members at once.

// Sending from a map output or another entity:
gruntController.Signal(“retreat”);


// Handling in your subclass:
protected override void OnSignal(string signal, object data)
{
    if (signal == “retreat”) GoTo(“search”);
}

Signals whose name starts with bark_ are automatically routed to Bark()Signal(“bark_pain”) fires a bark from the “pain” category.

Factions

Every NPC has a Faction property (NpcFaction). Perception only tracks entities whose faction is hostile to the NPC’s own. The template ships with three factions:

FactionManager.Player

Assigned automatically to the player entity. Not set on a controller directly.

FactionManager.Grunts

Hostile to Player and to Rebels.

FactionManager.Rebels

Allied with Player, hostile to Grunts. Use this for friendly NPCs.

// Make a companion NPC friendly to the player:
controller.Faction = FactionManager.Rebels;


// Register a new faction at startup:
var pirates = FactionManager.Register(“pirates”);
pirates.SetRelationshipTo(FactionManager.Player, FactionRelationship.Hostile);
pirates.SetRelationshipTo(FactionManager.Grunts, FactionRelationship.Neutral);

SetRelationshipTo() is symmetric — setting it once updates both sides. The four relationship values are Hostile, Neutral, Friendly, and Ally. Only Hostile causes NPCs to target each other. Ally is the strongest positive relationship and enables squad behavior between factions.

Squads

NPCs in the same squad share threat information: when one member calls RegisterThreat() with a boost of 0.3 or higher, all living members become aware of that threat. Squads also track a SharedTarget and a LastKnownTargetPosition that all members can read.

The template declares two squads in NpcSquads.cs:

npc.JoinSquad(NpcSquads.GruntSquad); // enemy group
npc.JoinSquad(NpcSquads.PlayerSquad); // companion group

JoinSquad() removes the NPC from its previous squad first. An NPC can only belong to one squad at a time.

Useful squad properties available inside a controller:

Squad.LivingCount / CombatCount

How many members are alive, and how many are currently in a combat state.

Squad.SharedTarget

The entity that squad members have most recently shared threat information about.

Squad.CurrentVisibilityCount

How many living members currently have line-of-sight to the shared target.

Squad.Broadcast(source, signal, data)

Sends a signal to all living members except the sender.

Squad Slot Claiming

Slot claiming lets squad members coordinate without a central coordinator. A “slot” is an arbitrary string tag — only one NPC can hold it at a time. This is how the grunt’s suppression/advance tactics work: only the NPC that claims “suppress” holds position and fires; the one that claims “advance” pushes forward.

bool isSuppressor = Squad.TryClaim(this, “suppress”);
bool isAdvancer = !isSuppressor && Squad.TryClaim(this, “advance”);


if (isSuppressor)
{
    StopMoving();
    FireAt(BestThreat);
}
else if (isAdvancer)
{
    Follow(BestThreat);
}


// In the state’s exit action, release all slots:
Squad.ReleaseAll(this);

TryClaim() returns true immediately if the slot is unclaimed or if the current holder is dead. Release(npc, slot) releases a specific slot; ReleaseAll(npc) releases everything that NPC holds. Always release all slots in your combat state’s exit: action so dead or transitioning NPCs don’t block the slot permanently.

Debug CVars

ai_debug 1

Draws perception cones, hear radii, threat lines, last-known-position markers, and squad links for all living NPCs.

ai_debug_nearest 1

Limits debug drawing to the NPC closest to the camera. Reduces visual noise when many NPCs are present.

ai_notarget 1

Prevents NPCs from targeting the player. NPCs still move, react to other stimuli, and fight each other.

ai_disable 1

Freezes all NPC update logic entirely. NPCs stop moving and reacting. Useful for setting up test scenarios or taking screenshots.