Skip to content

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.

  1. Add Health (scalar, bTriggersReplan = true) to your fact schema.
  2. Author DA_Goal_Survive:
  3. Desired State: Bool Fact EqualsIsSafe = true.
  4. Considerations: Goal Consideration: Fact Score with a curve that returns >0 only when Health < 0.2.
  5. InterruptionLevel: Critical.
  6. Author DA_Action_Flee with effect IsSafe = true.
  7. 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:

  1. Schema: add DistanceToTarget (scalar, replan-triggering).
  2. Create two actions: DA_Action_RangedAttack and DA_Action_MeleeAttack. Both have effect TargetDestroyed = true.
  3. Give DA_Action_RangedAttack a UCostStrategy_FactWeighted:
  4. BaseCost = 1.0
  5. FactWeights = { DistanceToTarget: -0.01 } — cost goes down as distance goes up.
  6. Give DA_Action_MeleeAttack a fact-weighted cost that goes up with distance: FactWeights = { DistanceToTarget: 0.02 }.
  7. 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:

  1. Mark only meaningful facts with bTriggersReplan = true (the schema honors this implicitly via version-bumping).
  2. Raise ReplanSafetyNetInterval on the component to 2.0 or more.
  3. Lower Replan Check Tick Rate indirectly by setting PrimaryComponentTick.TickInterval to ~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.

  1. Create a custom USquadCoordinator actor that exposes IsLayingCoverFire(), IsFlankingNorth(), etc.
  2. In each agent's IntentForge sensor, copy the coordinator's flags into that agent's FWorldState (e.g., AllyCovering = true).
  3. The "flanking" agent's Action_Flank has precondition AllyCovering = true.
  4. The "cover" agent's Action_LayCoverFire has effect AllyCovering = 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.

  1. Create BP_CostStrategy_Curved deriving from UIntentCostStrategy.
  2. Override Evaluate. In the graph, use a Curve Float asset, sample it with Get Scalar from the world state, and return the result.
  3. 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.

  1. In your archetype asset's Sensor Defaults, add an instance of Sensor: Gameplay Tag On Actor. Set its TagToObserve (e.g. Combat.IsArmed) and ResultFactId (IsArmed).
  2. Add an instance of Sensor: Distance To Referenced Actor. Set TargetActorObjectFactId = "PrimaryEnemy" and ResultScalarFactId = "DistanceToEnemy".
  3. Somewhere in gameplay code (e.g. a perception adapter), populate the PrimaryEnemy object fact with an FIntentActorFact whose Actor points 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.

  1. Add fact HasTarget (bool, replan-triggering).
  2. In each combat goal's DesiredState, the first row is Bool Fact EqualsHasTarget = true. (This makes the goal trivially unsatisfied when no target exists.)
  3. 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:

  1. Enable verbose planner logs:
Log LogIntentForgePlanner VeryVerbose
  1. In PIE, with Gameplay Debugger open, find the IntentForge category. It shows the active plan and goal.
  2. For a single-frame snapshot, run the IntentForge.DumpAllAgents console 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:

  1. Get the IntentForge component.
  2. Call Set Archetype with 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.

  1. Your pawn (or its AIController) must already have a UAIPerceptionComponent configured with whichever senses you want (Sight, Hearing, etc.).
  2. In your archetype's Sensor Defaults, add an instance of Sensor: AI Perception.
  3. Set HasTargetFactId to a bool fact in your schema (e.g. HasTarget). Set TargetActorFactId to an object fact (e.g. Target). Leave SenseFilter null to react to any sense, or set it to UAISense_Sight to listen only to vision.
  4. Now your archetype's goals can use HasTarget = true as 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.

  1. In PIE, press Ctrl+L (default binding) to open the Visual Logger.
  2. Select your pawn.
  3. 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).
  4. 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.

  1. Make sure your SO actors have USmartObjectComponent configured with a USmartObjectDefinition (engine's standard SO authoring).
  2. Author an IntentForge action DA_Action_UseBench with executor SO: Claim And Hold. Set:
  3. Activity Requirements: gameplay-tag query the candidate SO must satisfy (e.g. SmartObject.Activity.Rest).
  4. Search Radius: e.g. 1000 cm.
  5. Hold Seconds: how long the agent occupies the slot.
  6. Prepend a MoveTo action 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.

  1. Window → Developer Tools → IntentForge Live Inspector. Dock it wherever you like (a vertical panel on the right works well).
  2. Start PIE. The left pane fills with every live agent (actor name | current goal | step count). Refresh is 0.5 s.
  3. Click an agent. The right pane shows:
  4. Header: actor, archetype, world-state version
  5. Current plan with an arrow on the current step
  6. Every schema fact and its live value
  7. Recent replan history (newest 8)
  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:

  1. In PIE, open the editor console.
  2. Run IntentForge.ProfileSensors <PartialActorName> (omit the arg to see every agent).
  3. 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:

  1. Open your archetype asset.
  2. Under IntentForge Authoring in the details panel, click Analyze Archetype.
  3. The IntentForge MessageLog shows:
  4. Orphan facts: facts referenced by preconditions / goal desired-state that NO action produces. These guarantee unreachable goals.
  5. 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++):

  1. 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;
    }
};
  1. Register a dispatch handler at module startup so IntentForge::CheckPrecondition routes 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);
        });
}
  1. (Optional) Teach the validator about your type by extending its GetReferencedFactId switch — 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++):

  1. Declare Health as a Scalar fact and IsLowHealth as a Bool fact in your UFactSchemaAsset. Both with bTriggersReplan = true.
  2. On the same schema asset, populate the LatchedBoolFacts array with one row:
  3. Source Scalar Fact Id: Health
  4. Output Bool Fact Id: IsLowHealth
  5. Enter Threshold: 0.30
  6. Exit Threshold: 0.40
  7. Initial Value: false
  8. Author your Flee goal's DesiredState to read IsLowHealth == false (so the agent flees while the latch is true).
  9. Sensors / damage events write Health via SetFactScalar; the schema derives IsLowHealth reactively. Health jittering between 0.29 and 0.31 no longer flips IsLowHealth — 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++):

  1. On the relevant FFactSchemaEntry for DistanceToTarget (Scalar type), expand the Filter substruct in the details panel.
  2. Set bEnabled = true, Alpha = 0.25 (heavier smoothing for noisier inputs — try 0.10 for very noisy signals, 0.50 for mostly-clean ones).
  3. That's it. Every write to DistanceToTarget via SetFactScalar (or any sensor's OutState.SetScalar) is now Alpha*raw + (1-Alpha)*prior before 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.