name: ss14-npc-system-core
description: Deep dive into the NPC system in SS14/Sunrise: HTN planning, utility target selection, steering/pathfinding, blackboard and execution contracts. Use it when you need to understand the general scheme and logic of NPCs, create or rework behavior prototypes (htnCompound, rootTask, blackboard), or write your own AI code (operators, preconditions, components, systems) and safely integrate it into runtime.
NPC System: Architecture, Prototypes and Custom AI
Use this skill as your main NPC playbook in SS14/Sunrise.
Keep your focus on fresh code and check its relevance through git log/git blame (cutoff: 2024-02-20).
What to download first
references/architecture-runtime.md- general scheme of work, layers and life cycle of NPCs.references/behavior-prototypes.md- how to design and write behavior prototypes (htnCompound,utilityQuery,HTNcomponent).references/custom-ai-code.md- how and where to write your AI code (operators, preconditions, systems, components).references/debug-validation.md- debugging, checking and protection against regressions.
General scheme of NPC work
NPCSystemupdates only active NPCs (ActiveNPCComponent) and limits the update budget (npc.max_updates).HTNSystemschedules the behavior through the time-sliced job queue (HTNPlanJob) and executes the current plan.HTNPlanJobexpands the treeHTNCompoundTask->HTNPrimitiveTask, selecting branch by preconditions.HTNOperator.Plan()evaluates the validity of the step and can returneffectsfor blackboard.HTNOperator.Startup()/Update()/TaskShutdown()performs runtime logic.- The operator usually delegates heavy/coordinated logic to external systems through components (steering/combat, etc.).
NPCSteeringSystemcontrols movement, pathfinding and obstacle avoidance.NPCUtilitySystemselects targets viautilityQuery+ considerations/curves.NpcFactionSystemaffects the selection of hostile/friendly targets.
Design principles and logic
- Keep
Plan()as clean and predictable as possible: compute, not mutate, the world. - Perform side effects in
Startup()/Update(), and cleanup in shutdown hooks. - Divide “choose a goal”, “get to the goal”, “take an action” into different primitives.
- Use blackboard as a single contract between the prototype and the code.
- Design behavior as a composition of small compounds, not a giant-tree.
- Always add a fallback branch (idle/noop) so that the NPC does not get stuck without a plan.
- For movement and combat, rely on existing systems (
NPCSteeringSystem,NPCCombatSystem), and not on ad-hoc logic in the operator.
Patterns
- Start root-behavior from the priority branch (combat/goal), then fallback (idle/follow).
- Add
UtilityOperatorbefore the action if the goal is dynamic. - Update the target with the service (
services) inside combat primitives. - Make
MoveToOperatora separate step between goal selection and action. - Store ranges/flags in blackboard (
VisionRadius,MeleeRange,NavInteract) for fine-tuning without code. - Return
effectsfromPlan()to reuse expensive calculations in execution. - Use
IHtnConditionalShutdownwhen you need a controlled cleanup withTaskFinishedorPlanFinished. - Write preconditions as simple Boolean gate objects without side effects.
- Disable/enable HTN via
SetHTNEnabled/replan patterns instead of direct mutation of internal fields. - Validate the tree for recursive traps using a separate test.
Anti-patterns
- Write a monolithic
htnCompound, which is difficult to analyze and test. - Do side effects in
Plan()(especially network/physical changes). - Do not clean the blackboard and runtime components when completing a task.
- Rely on only one target key without fallback logic.
- Ignore
services, which causes the NPC to “stick” to an outdated target. - Mix pathfinding/steering/combat manually in one statement.
- Write a precondition that depends on a hidden mutable-state outside the blackboard/components.
- Use invalid types in
blackboardYAML (absence of!typefor complex/explicit types). - Build behavior without the idle/noop branch.
- Ignore budget/cooldown (
PlanCooldown,npc.max_updates) when diagnosing “stupid” NPCs.
Mini-examples
1) Root prototype with fallback
- type: htnCompound
id: MyCustomHostileCompound
branches:
- tasks:
- !type:HTNCompoundTask
task: MeleeCombatCompound
- tasks:
- !type:HTNCompoundTask
task: IdleCompound
2) Connecting HTN to an entity
- type: entity
id: MobMyNpc
components:
- type: HTN
rootTask:
task: MyCustomHostileCompound
blackboard:
VisionRadius: !type:Single
18
AggroVisionRadius: !type:Single
24
NavInteract: !type:Bool
true
3) Custom operator
public sealed partial class MyOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entManager = default!;
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
{
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
// Transfer heavy/runtime logic to a separate system or component.
return HTNOperatorStatus.Finished;
}
}
Extension rule
- When adding a new goal/behavior, first expand
utilityQueryand tasks prototypes. - Move to new C# code only when the prototypes and current operators are no longer enough.
- For new mechanics, first define an extension point (precondition/operator/system/component), then write the code.
- After any extension, run a domain debug check and a smoke test with a real NPC.
Think of the NPC system as a planning -> execution -> state maintenance pipeline rather than a random collection of operators.