Sentinel · Intermediate · Tutorial 2 of 3
Sentinel doesn't run your AI — it describes it. Once that clicks, the Blackboard, the Decorator and the version numbers all stop being mysterious, and you stop writing trees that can't possibly work.
Still no unsaved-changes warning. Same as tutorial one — closing the window discards everything silently. Press Save… before you close, every time.
In tutorial one you built a tree and watched agents obey it, which makes it very natural to assume Sentinel is the AI. It isn't, and almost every confusing thing about the tool dissolves once you see what it actually produces.
Export a behaviour and open the file. Every node in it looks like this — and only like this:
{
"id": "n7",
"kind": "Condition",
"task": "CanSeePlayer",
"children": null
}
Four fields. An identity, a structural kind, the name of a task, and its children. There is no radius, no target, no "how far can it see". CanSeePlayer is a word the game already knows. Sentinel is choosing which words to say and in what order; the game decides what they mean.
Sentinel authors a contract. The game fulfils it. Your tree is a script of task names and control flow. Every behaviour behind those names — what counts as seeing, how patrolling works, how far is "in range" — lives in the game's code, not in your file.
Open a behaviour and look at the Blackboard panel on the right — the one I told you to ignore last time. Press + Add variable and you get a row: a name, a type dropdown, a default value, and an × to remove it. Five types are available: Bool, Int, Float, String, Vector3.
Here is the part that trips everyone up. Nothing in the editor connects a variable to a node. You cannot make a Condition test alarmRaised, and no Action can write to it. Declaring a variable does not change what your tree does inside Sentinel, and it never will — that isn't a missing feature so much as the wrong side of the line.
So what is it for? It's the state half of the contract. When your behaviour ships, the game needs to know which variables this tree expects to exist, what type each one is, and what value it starts at. Declaring alarmRaised : Bool = false tells the runtime: allocate this, initialise it to false, and let my tasks use it.
Read it as a function signature. You are not writing the body; you are declaring what the body will be handed.
Load Templates → Patrol Guard again, then add three variables:
alarmRaised — Bool, default falselastSeenAt — Vector3, default 0,0,0patrolSpeed — Float, default 1.5Save, then export, then open the .behavior.json in a text editor. Your three variables are in there, in a blackboard array, with names, types and defaults intact — sitting alongside the tree rather than inside it. That's the shape of the handoff.
If task meanings live in the game, what genuine authoring power do you have? Control flow. Selector and Sequence from tutorial one, plus two more.
Drag + Decorator from the palette. It defaults to Inverter, and it takes exactly one child — try to give it two and the second connection won't take.
An Inverter flips its child's result: success becomes failure, failure becomes success. That sounds academic until you want a condition you don't have. There is no CannotSeePlayer in the palette — the conditions available are only CanSeePlayer, HealthLow, HasTarget and IsInRange. Wrap CanSeePlayer in an Inverter and you have built its opposite.
Try it: add a Sequence under the Selector, above the Patrol branch. Give it an Inverter wrapping CanSeePlayer, then Wait. Now the guard pauses when it can't see anyone — a nervous sentry rather than a metronome.
+ Parallel runs its children simultaneously rather than in order. Use it when a behaviour genuinely overlaps — walking a route while scanning — instead of forcing an artificial sequence.
Use it sparingly. A Selector or Sequence tells a reader exactly what happens next; a Parallel says "several things, and the result depends on a policy." It is the node most likely to make your tree hard to debug in tutorial three.
Every exported behaviour carries two:
| Field | Means |
|---|---|
behaviorVersion | Your version of this behaviour. You set it under Info…. Bump it when you change the tree. |
primitiveSetVersion | The version of the vocabulary — the set of task names Sentinel knows. You don't set this; it's stamped for you. |
The second one is doing real work. When Crucible loads a behaviour whose primitive set is newer than the one it supports, it says so out loud: "Primitive set v2 is newer than supported v1; unknown tasks will no-op."
That warning is the difference between a bug you can see and a bug you can't. Without it, a tree using a task the runtime doesn't recognise would simply do nothing, in one branch, intermittently — and you would go looking at your tree structure, which would be fine. When you see that warning, stop and check versions before you debug anything else.
behaviorVersion under Info… before exporting, not after. The value is written into the file at export time, so bumping it afterwards leaves you with a file whose contents disagree with your intent.id, kind, task, children. Nothing else travels.primitiveSetVersion is checked on load — heed the warning before debugging your tree.Next — Advanced: reading a run that misbehaves. Elite Combatant with several agents at once, then the whole debug bar — Load Trace, Stats, Heatmap, Analytics and the scrubber — used to find out why forty nodes and five agents are doing something you didn't intend.