Intent Forge — Cookbook¶
Recipes that solve common AI design problems on top of the Intent Forge primitives. All recipes assume you've completed the Quickstart.
1. Flee at low health (emergencies that preempt mid-action)¶
Problem: when health drops below 20%, override whatever the agent is doing — even if it's mid-MoveTo — and run for cover.
Recipe: use a Critical interruption-level goal.
- Add
Health(scalar,bTriggersReplan = true) to your fact schema. - Author
DA_Goal_Survive: - Desired State: Bool Fact Equals →
IsSafe = true. - Considerations: Goal Consideration: Fact Score with a curve
that returns >0 only when
Health < 0.2. - InterruptionLevel: Critical.
- Author
DA_Action_Fleewith effectIsSafe = true. - Because Critical outranks Normal regardless of score, the planner picks
Flee as soon as the consideration starts returning a positive score.
The MinActionCommitTime gate is automatically bypassed — the in-flight
action gets
CancelAction()immediately.
Why not just a high score? Score-based preemption is blocked by the
min-commit gate for MinActionCommitTime seconds after every action
starts. That's correct for normal goal-switching (prevents flapping), but
wrong for emergencies. Interruption levels are the escape hatch.
Watch out for: every Critical goal raises the noise floor for every other goal. Reserve Critical for true emergencies (low HP, taking damage, ally calling for help). Use Urgent for important-but-not-critical priorities (low ammo, lost target). Normal covers everything else.
2. Prefer ranged attacks at distance, melee up close¶
Problem: same goal (TargetDestroyed), two ways to satisfy it.
Recipe:
- Schema: add
DistanceToTarget(scalar, replan-triggering). - Create two actions:
DA_Action_RangedAttackandDA_Action_MeleeAttack. Both have effectTargetDestroyed = true. - Give
DA_Action_RangedAttackaUCostStrategy_FactWeighted: BaseCost = 1.0FactWeights = { DistanceToTarget: -0.01 }— cost goes down as distance goes up.- Give
DA_Action_MeleeAttacka fact-weighted cost that goes up with distance:FactWeights = { DistanceToTarget: 0.02 }. - The planner picks whichever has lower projected cost given the current distance.
Why this works: UCostStrategy_FactWeighted evaluates against the
current state during planning. Same goal, different costs by context.
3. Replan only on important events¶
Problem: the safety net replan every 0.75 s is wasteful — most ticks nothing has changed.
Recipe: this is already on by default. Replanning is gated on
WorldState.StateVersion. The version only bumps when a fact actually
changes. To take it further:
- Mark only meaningful facts with
bTriggersReplan = true(the schema honors this implicitly via version-bumping). - Raise
ReplanSafetyNetIntervalon the component to 2.0 or more. - Lower
Replan Check Tick Rateindirectly by settingPrimaryComponentTick.TickIntervalto ~0.1 s in C++ if you need very long-running idle agents.
4. One-shot action that releases external resources¶
Problem: you wrote a custom executor that claims a Smart Object, spawns a particle system, or starts a timeline. If the plan is invalidated mid-step, those resources leak.
Recipe: implement CancelAction(). Override it on your executor:
void UMyExecutor::CancelAction()
{
if (ClaimedSmartObject.IsValid())
{
SmartObjectSubsystem->Release(ClaimedSmartObject);
ClaimedSmartObject.Invalidate();
}
if (ActiveTimelineHandle.IsValid())
{
ActiveTimelineHandle.Stop();
}
}
The StateTree task calls CancelAction() exactly once when the plan goes
stale or the StateTree state is exited. There is no ExitAction follow-up
on cancellation.
5. Squad assault: one agent provides cover while another flanks¶
Problem: needs coordination, but IntentForge is per-agent.
Recipe (v1-compatible): model the squad as shared world state.
- Create a custom
USquadCoordinatoractor that exposesIsLayingCoverFire(),IsFlankingNorth(), etc. - In each agent's IntentForge sensor, copy the coordinator's flags into
that agent's
FWorldState(e.g.,AllyCovering = true). - The "flanking" agent's
Action_Flankhas preconditionAllyCovering = true. - The "cover" agent's
Action_LayCoverFirehas effectAllyCovering = true.
This is cooperative-by-convention rather than enforced. Full multi-agent planning is on the roadmap but not in 0.x.
6. Designer-authored cost curve¶
Problem: cost should scale non-linearly. Linear FactWeighted isn't
enough.
Recipe: subclass UIntentCostStrategy in Blueprint.
- Create
BP_CostStrategy_Curvedderiving fromUIntentCostStrategy. - Override
Evaluate. In the graph, use aCurve Floatasset, sample it withGet Scalarfrom the world state, and return the result. - Set this strategy on whichever actions need curved costs.
Blueprint-authored cost strategies are ~5–20× slower than C++. For actions called many times per plan that's measurable; for archetypes with 20-ish actions it's still well within the per-plan budget.
7. Sense the world via sensors on the archetype¶
Problem: facts must reflect the live game world (health, target
distance, ability cooldowns). Without sensors, every game system would
need to call SetFactBool directly.
Recipe: declare sensors on the archetype. The component duplicates
them per-agent on InitializeComponent, ticks them at their declared
SampleInterval, and tears them down on archetype change or end-play.
- In your archetype asset's Sensor Defaults, add an instance of
Sensor: Gameplay Tag On Actor. Set itsTagToObserve(e.g.Combat.IsArmed) andResultFactId(IsArmed). - Add an instance of
Sensor: Distance To Referenced Actor. SetTargetActorObjectFactId = "PrimaryEnemy"andResultScalarFactId = "DistanceToEnemy". - Somewhere in gameplay code (e.g. a perception adapter), populate the
PrimaryEnemyobject fact with anFIntentActorFactwhoseActorpoints at the perceived target. The distance sensor reads it on its next tick and writes the scalar fact.
Custom sensors: subclass UIntentSensorBase (C++) or
UIntentSensor_BP (Blueprint). Override Initialize to bind delegates
(e.g. AIPerception), Sample to write facts, Deinitialize to release.
Set SampleInterval for polling sensors; for event-driven sensors
override Sample to a no-op and do the work in the bound delegate.
Event-driven without a sensor: for one-off events (e.g. taking
damage), call IntentForgeComponent->SetFactBool directly from your
gameplay code. The sensor framework exists for continuous observation,
not every world-state mutation.
8. Stop planning when no target is visible¶
Problem: combat archetype shouldn't even consider attack goals if there's no target.
Recipe: gate the goal with a precondition.
- Add fact
HasTarget(bool, replan-triggering). - In each combat goal's
DesiredState, the first row is Bool Fact Equals →HasTarget = true. (This makes the goal trivially unsatisfied when no target exists.) - Since the goal can't be satisfied without a target, the planner returns no plan, and the component idles.
Alternative: write a UIntentGoalConsiderationSet Blueprint that returns
0 when HasTarget is false. The goal scores at 0 and gets skipped.
9. Inspect why an action wasn't chosen¶
Problem: the agent picked the wrong action; you want to understand why.
Recipe:
- Enable verbose planner logs:
- In PIE, with Gameplay Debugger open, find the IntentForge category. It shows the active plan and goal.
- For a single-frame snapshot, run the
IntentForge.DumpAllAgentsconsole command — it prints one line per agent with the goal, plan length, cost, and world version.
The richer "why didn't this action get chosen?" inspector is on the roadmap.
10. Hot-swap the archetype at runtime¶
Problem: a peasant becomes a soldier when their barracks is built.
Recipe:
- Get the IntentForge component.
- Call
Set Archetypewith the new archetype asset.
This will: - Cancel any in-flight action. - Reset the world state. - Rebind the schema. - Force a replan on the next tick.
Carry-over facts (e.g., the agent's name, current location) belong on the
actor, not in FWorldState — the world state is intentionally wiped on
archetype change.
11. Wire sight/hearing/damage perception in two minutes¶
Problem: an enemy needs to plan based on what it can see. Writing the AIPerception → fact glue from scratch is tedious.
Recipe: enable the IntentForgePerception module, then drop the
sensor on your archetype.
- Your pawn (or its AIController) must already have a
UAIPerceptionComponentconfigured with whichever senses you want (Sight, Hearing, etc.). - In your archetype's Sensor Defaults, add an instance of
Sensor: AI Perception. - Set
HasTargetFactIdto a bool fact in your schema (e.g.HasTarget). SetTargetActorFactIdto an object fact (e.g.Target). LeaveSenseFilternull to react to any sense, or set it toUAISense_Sightto listen only to vision. - Now your archetype's goals can use
HasTarget = trueas a precondition, and actions can pair with the distance sensor to compute range-to-target.
When a target is perceived, HasTarget flips to true and the object
fact is populated with an FIntentActorFact. When the same target is
lost (sense-loss event), both clear.
For multi-target priority schemes (e.g. "focus the closest visible
enemy"), subclass UIntentSensor_AIPerception and override
HandlePerceptionUpdated — the rest of the lifecycle is reused.
12. Time-travel debug a misbehaving agent¶
Problem: an agent picks the wrong action and you want to see the exact sequence of events leading up to it.
Recipe: use the Visual Logger.
- In PIE, press Ctrl+L (default binding) to open the Visual Logger.
- Select your pawn.
- Scrub the timeline. You'll see entries each time the planner adopts a new plan (goal, step count, cost) and each time an action completes (handle + terminal status).
- Pair with the Gameplay Debugger overlay (apostrophe key) for live world-state inspection at the same timestamp.
VL entries are stripped in Shipping builds — zero runtime cost in release.
13. Reserve a Smart Object slot with claim-and-hold¶
Problem: multiple agents want to use the same bench / workstation / cover point. Without coordination they'll fight over it.
Recipe: enable the IntentForgeSmartObjects module, then use
SO: Claim And Hold as the executor for the "use this kind of thing"
action.
- Make sure your SO actors have
USmartObjectComponentconfigured with aUSmartObjectDefinition(engine's standard SO authoring). - Author an IntentForge action
DA_Action_UseBenchwith executorSO: Claim And Hold. Set: - Activity Requirements: gameplay-tag query the candidate SO
must satisfy (e.g.
SmartObject.Activity.Rest). - Search Radius: e.g. 1000 cm.
- Hold Seconds: how long the agent occupies the slot.
- Prepend a
MoveToaction whose target is the slot's location. The claim executor does not walk the agent there — keeping the responsibilities separate means an agent can claim a slot before moving (a common reservation pattern).
The executor releases the slot on success and on cancellation, so plan-invalidation mid-hold cleanly returns the slot to the pool. Richer SO usage (running the SO's BehaviorDefinition) is left to subclasses or future releases.
14. Live-inspect agents at-a-glance during PIE¶
Problem: Gameplay Debugger overlays a single selected agent, but you want to see every agent in the world at once and click between them.
Recipe: use the IntentForge Live Inspector tab.
- Window → Developer Tools → IntentForge Live Inspector. Dock it wherever you like (a vertical panel on the right works well).
- Start PIE. The left pane fills with every live agent (actor name | current goal | step count). Refresh is 0.5 s.
- Click an agent. The right pane shows:
- Header: actor, archetype, world-state version
- Current plan with an arrow on the current step
- Every schema fact and its live value
- Recent replan history (newest 8)
- Pair with the Visual Logger for after-the-fact "why did it do that?" replay.
The inspector uses UIntentForgeWorldSubsystem::GetAllAgents() — same
registry exposed for your own debug tooling if you want a custom panel.
15. Find the slow sensor¶
Problem: your AI feels sluggish. You suspect a sensor is doing too much work per Sample. Profile it.
Recipe:
- In PIE, open the editor console.
- Run
IntentForge.ProfileSensors <PartialActorName>(omit the arg to see every agent). - The output lists each sensor's class, total sample count, accumulated time, and average ms per sample. The hot sensor is the one with high avg-ms or runaway sample count.
Common causes of slow sensors:
- Sample interval too low (samples 60×/sec when 10× would do).
- Expensive per-sample work (line traces, EQS queries) without
caching.
- Calling FindComponentByClass every sample (use
UIntentForgeStatics::GetIntentForgeComponent and cache).
For Blueprint-authored sensors, expect 5–20× slower than C++ per sample. Rewrite to C++ if profiling shows it matters for your scale.
16. Surface authoring bugs with Analyze Archetype¶
Problem: your agent does nothing at runtime. Validator passed. Test Plan returns empty. What's wrong?
Recipe:
- Open your archetype asset.
- Under IntentForge Authoring in the details panel, click Analyze Archetype.
- The IntentForge MessageLog shows:
- Orphan facts: facts referenced by preconditions / goal desired-state that NO action produces. These guarantee unreachable goals.
- Unused actions: actions whose effects no consumer reads — usually a sign of a missing precondition or a typo'd fact id.
A clean run prints "OK: every consumed fact has at least one producer" and "OK: every action's effects are referenced." If you see orphans, trace the fact name and either spell it correctly or add a producing action.
17. Author a custom precondition type¶
Problem: the bundled BoolFactEquals and ScalarComparison
preconditions don't cover your need (e.g. "fact within a numeric
range", "tag query against actor's GAS"). Add a custom one.
Recipe (C++):
- Subclass
FIntentPrecondition:
USTRUCT(BlueprintType, DisplayName = "Scalar In Range")
struct FPrecondition_ScalarInRange : public FIntentPrecondition
{
GENERATED_BODY()
UPROPERTY(EditAnywhere) FName FactId;
UPROPERTY(EditAnywhere) float Min = 0.f;
UPROPERTY(EditAnywhere) float Max = 1.f;
bool EvaluateAgainst(const FWorldState& State) const
{
const float V = State.GetScalar(FactId);
return V >= Min && V <= Max;
}
};
- Register a dispatch handler at module startup so
IntentForge::CheckPreconditionroutes calls to your type:
void FMyModule::StartupModule()
{
IntentForge::FPreconditionRegistry::Register(
FPrecondition_ScalarInRange::StaticStruct(),
[](const FInstancedStruct& S, const FWorldState& State) {
return S.Get<FPrecondition_ScalarInRange>().EvaluateAgainst(State);
});
}
- (Optional) Teach the validator about your type by extending its
GetReferencedFactIdswitch — otherwise typo'd fact ids won't be caught at save time for your custom shape.
Effect-shaped types follow the same pattern via FIntentEffect and
the effect registry.
18. Stop a goal flapping near a threshold (Schmitt latch on a scalar fact)¶
Problem: you want a goal to fire when Health < 0.30, but the player
takes a series of small hits and Health oscillates around the threshold
(0.29, 0.31, 0.29, 0.32...). Without protection the goal flaps in/out
every replan and the agent visibly stutters between Flee and Patrol.
Recipe (no C++):
- Declare
Healthas a Scalar fact andIsLowHealthas a Bool fact in yourUFactSchemaAsset. Both withbTriggersReplan = true. - On the same schema asset, populate the
LatchedBoolFactsarray with one row: - Source Scalar Fact Id:
Health - Output Bool Fact Id:
IsLowHealth - Enter Threshold:
0.30 - Exit Threshold:
0.40 - Initial Value:
false - Author your Flee goal's
DesiredStateto readIsLowHealth == false(so the agent flees while the latch is true). - Sensors / damage events write
HealthviaSetFactScalar; the schema derivesIsLowHealthreactively.Healthjittering between 0.29 and 0.31 no longer flipsIsLowHealth— only a sustained drop below 0.30 trips the latch true, and only a sustained recovery above 0.40 trips it back.
Why this works: IsLowHealth carries memory (the latched state) that
the raw Health scalar can't. Preconditions stay pure functions of
world state; the hysteresis lives at the fact-derivation layer. See the
Anti-Flap Toolkit for the full layered toolkit.
19. Smooth a noisy scalar fact (EMA filter)¶
Problem: a sensor writes DistanceToTarget every frame, but the
target's position is jittery (e.g. animated rig) or the sensor itself
has fast-vs-slow sampling artifacts. Goal scores tied to
DistanceToTarget flap.
Recipe (no C++):
- On the relevant
FFactSchemaEntryforDistanceToTarget(Scalar type), expand theFiltersubstruct in the details panel. - Set
bEnabled = true,Alpha = 0.25(heavier smoothing for noisier inputs — try0.10for very noisy signals,0.50for mostly-clean ones). - That's it. Every write to
DistanceToTargetviaSetFactScalar(or any sensor'sOutState.SetScalar) is nowAlpha*raw + (1-Alpha)*priorbefore storage. The Live Inspector's "Derived Facts" panel shows the raw input alongside the smoothed value so you can verify the curve.
Note on first-sample passthrough: the very first write of a filtered fact stores raw (no smoothing bias toward zero). The EMA only kicks in on the second and subsequent samples.
Note on planner search: the planner suppresses filtering during A* expansion (effects in simulated futures are deterministic, not noisy). Latching is not suppressed — derived bools are part of the symbolic state the planner reasons about. See the Anti-Flap Toolkit for why this asymmetry is the load-bearing rule.