WorldState and the scripting interface
WorldState is the sole input to an agent’s decisions. This chapter covers its value types, C++ read/write, script (Lua / ScriptCanvas) read/write, and the completion callback contract for script operators.
Value type HtnValue
HtnValue is a closed eight-type variant (public header HtnPlanner/HtnValue.h). Closed on purpose: the planner must be able to compare and simulate every value, the editor must be able to edit it, and serialization must round-trip.
using HtnValue = AZStd::variant<
bool, // Bool
int64_t, // Int
float, // Float
AZ::Vector3, // Vector3
AZ::EntityId, // EntityId
AZ::Crc32, // Tag: authored as a string, Crc'd at compile time
AZ::Uuid, // Uuid: asset / external object references
AZStd::string>; // String: human-readable text (names, labels, dialogue ids)
The matching type enum HtnValueType: Bool / Int / Float / Vector3 / EntityId / Tag / Uuid / String.
| Type | Comparable | Orderable | Arithmetic (Add/Sub) | Typical use |
|---|---|---|---|---|
| Bool | yes | no | no | Toggle facts (EnemyVisible) |
| Int | yes | yes | yes | Counters (Ammo, Health) |
| Float | yes | yes | yes | Continuous quantities (equality is exact; determinism forbids epsilon tolerance) |
| Vector3 | yes | no | no | Positions, directions |
| EntityId | yes | no | no | Target entity references |
| Tag | yes | no | no | Enum-like labels (string Crc; compact and fast to compare) |
| Uuid | yes | no | no | Asset references |
| String | yes | no | no | When the literal text matters (dialogue ids, etc.) |
Use AZStd::get<T>(value) to access a known type; the same header also offers helpers such as GetValueType / MakeDefaultValue / ValuesEqual / CompareValues / AddValues / SubtractValues / ValueToString.
Reading and writing WorldState from C++
Go through HtnAgentRequestBus, addressed by key name (AZ::Crc32). A type mismatch logs a warning and returns false (no crash):
#include <HtnPlanner/HtnPlannerBus.h>
#include <HtnPlanner/HtnValue.h>
// Write: a sensor reports an enemy sighting
HtnPlanner::HtnAgentRequestBus::Event(
agentId, &HtnPlanner::HtnAgentRequests::SetWorldStateValue,
AZ::Crc32("Guard.EnemyVisible"), HtnPlanner::HtnValue(true));
// Write: update the ammo count (Int uses int64_t)
HtnPlanner::HtnAgentRequestBus::Event(
agentId, &HtnPlanner::HtnAgentRequests::SetWorldStateValue,
AZ::Crc32("Guard.Ammo"), HtnPlanner::HtnValue(int64_t{ 12 }));
// Read: fetch a key's current value
AZStd::optional<HtnPlanner::HtnValue> ammo;
HtnPlanner::HtnAgentRequestBus::EventResult(
ammo, agentId, &HtnPlanner::HtnAgentRequests::GetWorldStateValue, AZ::Crc32("Guard.Ammo"));
if (ammo.has_value())
{
const int64_t value = AZStd::get<int64_t>(ammo.value());
// .
}
Key points:
- Keys must be declared in the Domain (WorldState Key nodes); writes to undeclared keys fail with a warning. Key types are locked at compile time.
- Writes affect only that agent’s WorldState; HTN does not replicate WorldState over the network (in multiplayer the authority usually owns the agent and runs planning).
- Every successful write bumps the revision—the trigger for opportunistic replan.
Scripting (Lua / ScriptCanvas)
The system registers a set of global functions in BehaviorContext under the HTN category:
| Global function | Equivalent EBus call |
|---|---|
HtnRequestPlan(entityId) | RequestPlan |
HtnRequestReplan(entityId) | RequestReplan |
HtnStop(entityId, abortCurrentOperator) | Stop |
HtnIsExecuting(entityId) | IsExecuting |
HtnGetCurrentStepName(entityId) | GetCurrentStepName |
HtnSetWorldState(entityId, key, value) | SetWorldStateValue |
HtnGetWorldState(entityId, key) | GetWorldStateValue |
HtnCompleteCurrentOperator(entityId, token, success) | CompleteCurrentOperator |
Lua example:
-- Write a bool fact and request a replan
HtnSetWorldState(self.entityId, "Guard.EnemyVisible", true)
HtnRequestReplan(self.entityId)
Value boxing at the script boundary
Arguments cross the script boundary boxed as AZStd::any. Script numbers widen as usual (double → Float, integers → Int). The key pitfall: by default, a string passed from script is boxed as a Tag (Crc32), not a String.
- If your key really is
Tag-typed, just pass the string. - If the key must preserve the literal text, declare the key as
Stringin the Domain—the write path then keeps the string literal based on the key’s expected type.
Script operator completion callbacks
When a primitive task binds the built-in bridge operator Script.SendOperatorEvent, the operator body is implemented in script (bus definitions in the public header HtnPlanner/HtnScriptOperatorBus.h):
HtnScriptOperatorNotificationBus (addressed by agent entity id, reflected to BehaviorContext):
virtual void OnOperatorBegin(const AZStd::string& operatorName, uint32_t operationToken,
const AZStd::vector<AZStd::any>& args);
virtual void OnOperatorTick(const AZStd::string& operatorName, uint32_t operationToken);
virtual void OnOperatorAborted(const AZStd::string& operatorName, uint32_t operationToken);
virtual void OnHtnEvent(const AZStd::string& eventName, const AZStd::vector<AZStd::any>& args); // one-shot notification from SendScriptEvent
Lua implementation sketch:
function MyOperatorHandler:OnOperatorBegin(operatorName, token, args)
self.token = token -- remember the generation token
-- kick off an async action (door animation, movement, .)
end
-- Report completion when the async action finishes; must pass back the same token
function MyOperatorHandler:OnAnimFinished()
HtnCompleteCurrentOperator(self.entityId, self.token, true)
end
The token is a “generation token”: if a replan happened in between, the old operator was
Aborted and completion reports with the stale token are ignored (with a warning), filtering out late callbacks from async actions. Async script operators must save and return the token received in Begin, or the operator stays Running forever.
The operatorName for Script.SendOperatorEvent comes from the operatorName setting of the .htnnode preset that wraps it; see
Extending operators for how to bind script operators in the graph.