Merge upstream/main into feat/roblox-skills

Resolve the count drift with the minimum README change the validator requires:
only the Catalog line moves to 73. The headline, badge, lead paragraph, quick-start
line and layout block deliberately stay as they are to avoid disturbing indexed
copy; docs/COMPATIBILITY.md and docs/INSTALLATION.md are left identical to main.
Adds the five Roblox skills to the other-engines catalog table.
This commit is contained in:
Kiro Crew
2026-09-08 14:41:38 +00:00
11 changed files with 1026 additions and 17 deletions
+3 -1
View File
@@ -71,6 +71,7 @@
"./skills/disciplines/game-ui-ux",
"./skills/disciplines/camera-systems",
"./skills/disciplines/performance-optimization",
"./skills/disciplines/ai-behavior-trees-utility-ai",
"./skills/genres/platformer",
"./skills/genres/roguelike",
"./skills/genres/rpg",
@@ -199,7 +200,8 @@
"./skills/disciplines/game-feel",
"./skills/disciplines/game-ui-ux",
"./skills/disciplines/camera-systems",
"./skills/disciplines/performance-optimization"
"./skills/disciplines/performance-optimization",
"./skills/disciplines/ai-behavior-trees-utility-ai"
]
},
{
+13 -12
View File
@@ -4,19 +4,19 @@
<p align="center">
<img src="docs/assets/banner.png" width="820"
alt="awesome-gamedev-agent-skills — game-dev skills for AI coding agents. 72 skills and a router across 10 engines, including an art-direction and asset-production workflow.">
alt="awesome-gamedev-agent-skills — game-dev skills for AI coding agents. 67 skills and a router across 10 engines, including an art-direction and asset-production workflow.">
</p>
**72 game-dev skills for your AI coding agent — install once, and a router loads the
**68 game-dev skills for your AI coding agent — install once, and a router loads the
right skill for whatever you're building.**
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
[![Skills](https://img.shields.io/badge/skills-72%20%2B%20router-brightgreen)](skills/)
[![Skills](https://img.shields.io/badge/skills-68%20%2B%20router-brightgreen)](skills/)
[![Format](https://img.shields.io/badge/format-Agent%20Skills-informational)](docs/SKILL-FORMAT.md)
[![Last commit](https://img.shields.io/github/last-commit/gamedev-skills/awesome-gamedev-agent-skills)](https://github.com/gamedev-skills/awesome-gamedev-agent-skills/commits/main)
[Agent Skills](docs/SKILL-FORMAT.md) are small capability files an AI agent loads only when it
needs them. This repo gives your agent **72 game-dev skills** and a **router** that picks the
needs them. This repo gives your agent **68 game-dev skills** and a **router** that picks the
right ones for you. You describe what you're building; the agent loads the matching engine and
task skills before it writes code.
@@ -40,7 +40,7 @@ task skills before it writes code.
## Quick start
**One command, any agent.** The [`skills`](https://www.npmjs.com/package/skills) CLI detects the
coding agent you already use and installs the router plus all 72 skills into the right place:
coding agent you already use and installs the router plus all 68 skills into the right place:
```bash
npx skills add gamedev-skills/awesome-gamedev-agent-skills
@@ -117,7 +117,7 @@ engine), while disciplines, genres, and workflows are additive on top.
## Catalog
72 skills across 8 categories — each links to its `SKILL.md` below.
73 skills across 8 categories — each links to its `SKILL.md` below.
### Engines
@@ -185,13 +185,13 @@ engine), while disciplines, genres, and workflows are additive on top.
| [`love2d-core`](skills/other-engines/love2d-core/SKILL.md) | LÖVE `load/update/draw` loop, dt-driven motion, input, states (LÖVE 11.5) |
| [`roblox-luau`](skills/other-engines/roblox-luau/SKILL.md) | Roblox Luau scripting: services, instances, client/server model |
| [`roblox-datastores`](skills/other-engines/roblox-datastores/SKILL.md) | Persistent data with `DataStoreService`: sessions, limits, ordered stores |
| [`roblox-ui`](skills/other-engines/roblox-ui/SKILL.md) | Production Roblox UI: responsive Instances, safe insets, input/focus, lifecycle, visual quality |
| [`roblox-networking`](skills/other-engines/roblox-networking/SKILL.md) | Server-authoritative remotes, validation, rate limits, replication, streaming, prediction |
| [`roblox-characters`](skills/other-engines/roblox-characters/SKILL.md) | Respawn-safe characters, Humanoids, rigs, movement, animations, tools, cleanup |
| [`roblox-ui`](skills/other-engines/roblox-ui/SKILL.md) | Roblox UI: `ScreenGui` layout, responsive scaling, safe areas, gamepad/touch navigation |
| [`roblox-networking`](skills/other-engines/roblox-networking/SKILL.md) | Remotes, server-authoritative validation, rate limiting, replication boundaries |
| [`roblox-characters`](skills/other-engines/roblox-characters/SKILL.md) | Character lifecycle: spawn/respawn scopes, `Humanoid`, rigs, `Animator` tracks |
| [`roblox-physics`](skills/other-engines/roblox-physics/SKILL.md) | Assemblies, constraints, collision/query policy, raycasts, forces, network ownership |
| [`roblox-studio-workflow`](skills/other-engines/roblox-studio-workflow/SKILL.md) | Explorer-first place editing, structure preservation, multi-context Studio verification |
### Disciplines — 14 ([`skills/disciplines/`](skills/disciplines/))
### Disciplines — 15 ([`skills/disciplines/`](skills/disciplines/))
Cross-engine concepts that load alongside the detected engine skill.
@@ -199,6 +199,7 @@ Cross-engine concepts that load alongside the detected engine skill.
|-------|-------|
| [`create-game-assets`](skills/disciplines/create-game-assets/SKILL.md) | Art direction and production pipeline for cohesive sprites, tiles, textures, icons, UI art, and 3D assets |
| [`game-ai`](skills/disciplines/game-ai/SKILL.md) | NPC decision-making: FSMs, behavior trees, steering, pathfinding |
| [`ai-behavior-trees-utility-ai`](skills/disciplines/ai-behavior-trees-utility-ai/SKILL.md) | Production behavior-tree runtime (Blackboard, composites, decorators, leaves) + Utility AI (curves, considerations, evaluator) + hybrid AI |
| [`procedural-gen`](skills/disciplines/procedural-gen/SKILL.md) | Noise, RNG, seeds, grid/dungeon/terrain generation |
| [`dialogue-systems`](skills/disciplines/dialogue-systems/SKILL.md) | Branching dialogue/narrative: nodes, conditions, variables (Yarn/Ink) |
| [`save-systems`](skills/disciplines/save-systems/SKILL.md) | Serialize/restore game state: formats, slots, versioning, autosave |
@@ -279,7 +280,7 @@ Here's the shape of a single exchange (illustrative):
> add a double jump to my player
Detected Godot (project.godot). Loading godot-2d-movement for the controller
and platformer for jump feel — skipping the other 65 skills.
and platformer for jump feel — skipping the other 66 skills.
```
```gdscript
@@ -308,7 +309,7 @@ func _physics_process(delta: float) -> void:
## Repository layout
```
skills/ 72 specialized skills, grouped by engine / discipline / genre / workflow
skills/ 68 specialized skills, grouped by engine / discipline / genre / workflow
router/ the master router skill (+ references/)
docs/ authoring standard, installation, compatibility
templates/ SKILL.md template
+1 -1
View File
@@ -27,7 +27,7 @@ The [`skills` CLI](https://www.npmjs.com/package/skills) is the package manager
Skills ecosystem. It auto-detects your agent(s) and writes skills to each one's directory:
```bash
# install the router + all 72 skills into whatever agent(s) you have
# install the router + all 68 skills into whatever agent(s) you have
npx skills add gamedev-skills/awesome-gamedev-agent-skills
# preview without installing
+2 -2
View File
@@ -30,7 +30,7 @@ is the dispatcher). Copy it in alongside the others so the agent can route reque
The [`skills` CLI](https://www.npmjs.com/package/skills) is the package manager for the Agent
Skills ecosystem. It detects the agents installed on your machine and copies the skills (the
router plus all 72) into each one's skills directory — no clone required:
router plus all 68) into each one's skills directory — no clone required:
```bash
# install into whatever agent(s) you have
@@ -59,7 +59,7 @@ so you can install it without cloning or copying files. Add the marketplace once
claude plugin marketplace add gamedev-skills/awesome-gamedev-agent-skills
```
Easiest — install the router and all 72 skills in one command:
Easiest — install the router and all 68 skills in one command:
```bash
claude plugin install gamedev@awesome-gamedev-agent-skills
+2 -1
View File
@@ -71,7 +71,7 @@ and `../docs/VERSION-SUPPORT.md`.
After the engine, read the request for task signals (three **additive** categories):
- **disciplines** (cross-engine concepts): `create-game-assets`, `game-ai`, `procedural-gen`, `dialogue-systems`,
- **disciplines** (cross-engine concepts): `create-game-assets`, `game-ai`, `ai-behavior-trees-utility-ai`, `procedural-gen`, `dialogue-systems`,
`save-systems`, `audio-design`, `shader-programming`, `physics-tuning`, `level-design`,
`input-systems`, `game-feel`, `camera-systems`, `game-ui-ux`, `performance-optimization`.
Triggered by concept words ("sprite sheet", "art direction", "texture", "pathfinding",
@@ -114,6 +114,7 @@ File signals sharpen this: `*.yarn`/`*.ink` → `dialogue-systems`/`visual-novel
|-------------------|------------------|-------------------------|
| art direction, game assets, sprites, tilesets, textures, icons, 3D props | `create-game-assets` | engine importer/rendering skill; `imagegen` when available |
| enemy AI, behavior tree, pathfinding, steering | `game-ai` | `unity-navmesh` / `unreal-behavior-trees` / Godot nav |
| BT runtime, blackboard, decorator, selector/sequence, utility AI, response curve, consideration | `ai-behavior-trees-utility-ai` | `game-ai` (model choice) / `unreal-behavior-trees` (engine assets) |
| procedural, noise, seed, dungeon generator | `procedural-gen` | engine tilemap/grid skill |
| dialogue, Yarn, Ink, conversation tree | `dialogue-systems` | engine UI skill |
| save/load, slots, persistence | `save-systems` | `roblox-datastores` / engine IO |
+1
View File
@@ -83,6 +83,7 @@ a real folder at `skills/<category>/<name>/SKILL.md`. Trigger words (`says:`) an
|---------------|------------------|-------------------------------|
| `create-game-assets` | art direction, game assets, sprite/sprite sheet, tileset, texture, icon, concept art, 3D prop, consistent style | engine importer/rendering skill; installed `imagegen` for raster generation when available |
| `game-ai` | enemy AI, behavior tree, state machine, steering, pathfinding | `unity-navmesh` / `unreal-behavior-trees` / Godot nav (`godot-nodes-scenes` + `godot-physics`) |
| `ai-behavior-trees-utility-ai` | behavior tree runtime, blackboard, action/condition node, selector/sequence/parallel, decorator, utility AI, scoring/response curve, consideration, hybrid AI | `game-ai` (FSM/BT/steering choice) / `unreal-behavior-trees` (engine BT/Blackboard assets) |
| `procedural-gen` | procedural generation, perlin/simplex noise, random seed, dungeon generator | engine tilemap/grid: `godot-tilemap` / `unity-tilemap-2d` / `godot-3d-essentials` |
| `dialogue-systems` | dialogue system, branching dialogue, Yarn Spinner, Ink, conversation tree; `*.yarn`/`*.ink` | engine UI: `godot-ui-control` / Unity UI |
| `save-systems` | save system, save/load, persistence, save slots; `save*.json`/`*.sav` | `roblox-datastores` / Godot IO / engine serialization |
@@ -0,0 +1,165 @@
---
name: ai-behavior-trees-utility-ai
description: >
Build a production behavior-tree runtime (Blackboard, action/condition leaves,
sequence/selector/parallel composites, decorators) and a Utility AI system (response
curves — linear, exponential, sigmoid, quadratic — considerations, and action evaluators),
plus hybrid BT-drives-Utility agents. Use when implementing a reusable behavior-tree or
utility-based decision system, or tuning enemy/NPC decisions beyond a simple FSM, or when
the user mentions behavior tree, blackboard, decorator, selector, sequence, tick status,
utility AI, response/scoring curve, or consideration. For choosing between FSM/BT/steering
or for pathfinding, use game-ai; for Unreal's BehaviorTree/Blackboard assets, use
unreal-behavior-trees.
---
# Behavior Trees & Utility AI
Two complementary ways to structure NPC decision-making, plus how to combine them. A
**behavior tree (BT)** expresses *structured, prioritized, reactive* logic as a tree that is
"ticked" each step. **Utility AI** answers *"how much do I want each option right now?"* by
scoring actions with normalized curves and picking the best. Ship believable agents by using a
BT for structure and Utility AI where graded trade-offs matter.
This skill is the **implementation** companion to `game-ai` (which helps you *choose* between
FSM / BT / steering / pathfinding). Read `game-ai` to pick a model; read this to build the
runtime.
## When to use
- Use to build a **reusable BT runtime**: a `Blackboard`, `Node` base, action/condition leaves,
`Sequence`/`Selector`/`Parallel` composites, and decorators (Inverter, Cooldown, Repeat).
- Use to build a **Utility AI** decider: response curves, considerations, and an evaluator that
scores and selects actions (max, softmax, or weighted-random for variety).
- Use to build **hybrid AI** — a BT whose leaf delegates the "which attack / which target"
choice to a utility evaluator.
**When *not* to use:** to *choose* between FSM, BT, steering, or pathfinding, and for A*/navmesh
routing, use `game-ai`. For Unreal's asset-based `BehaviorTree`/`Blackboard`, `BTTask`/`BTService`
and `AIController`, use `unreal-behavior-trees`. For the navmesh agent that *moves* the NPC, use
`unity-navmesh` or the engine's navigation node.
## Core workflow
1. **Pick the model.** Structured, prioritized, interruptible behavior → **BT**. Continuous
"score every option" decisions (targeting, needs, item choice) → **Utility**. Both → **hybrid**.
2. **Design the Blackboard first.** One typed key/value store per agent is the shared memory that
decouples nodes; leaves read/write it and never hold references to each other.
3. **Write leaves.** *Conditions* return `Success`/`Failure` immediately; *actions* return
`Running` across frames until they finish. Keep leaves small and side-effect-explicit.
4. **Compose.** `Selector` = OR/fallback (first non-failure wins); `Sequence` = AND (stop at first
non-success); `Parallel` for concurrent branches. Wrap with decorators for policy (invert,
cooldown, repeat, force-success).
5. **For Utility:** enumerate considerations, map each raw fact through a **normalized 0..1 curve**,
combine (weighted product with compensation, or weighted sum), then select the max — add
hysteresis so agents don't flip-flop on ties.
6. **Tick deliberately.** Tick the tree/evaluator once per *decision step* (often slower than
render). Preserve `Running` state between ticks; verify by drawing the active path and the
per-action scores on screen while tuning.
## Architecture at a glance
A behavior tree evaluates top-down, left-to-right; each node returns a status up to its parent:
```mermaid
flowchart TD
Root["Selector (root)"] --> Combat["Sequence: Combat"]
Root --> Patrol["Action: Patrol"]
Combat --> See["Condition: CanSeePlayer?"]
Combat --> InRange{"Selector: Reach"}
Combat --> Attack["Action: Attack (Running)"]
InRange --> Close["Condition: InAttackRange?"]
InRange --> MoveTo["Action: MoveToPlayer (Running)"]
```
Utility AI is a scoring pipeline — every candidate action is scored, then one is selected:
```text
facts (distance, health, ammo…)
│ each fact → a normalized 0..1 response curve (consideration)
score(action) = weight · combine(consideration_1 … consideration_n) # product+compensation or sum
select: argmax · or softmax / weighted-random for variety · + hysteresis to avoid jitter
```
**Status is a three-value enum** shared by every node — this is the contract that makes the tree
composable:
```csharp
public enum Status { Success, Failure, Running }
public abstract class Node
{
public abstract Status Tick(Blackboard bb, float dt);
public virtual void Reset() { } // called when a parent abandons this subtree
}
```
```csharp
// Selector = fallback/OR: return the first child that is not Failure.
public sealed class Selector : Composite
{
public override Status Tick(Blackboard bb, float dt)
{
for (; _current < Children.Count; _current++)
{
var s = Children[_current].Tick(bb, dt);
if (s != Status.Failure) return s; // Success or Running stops the scan
}
_current = 0;
return Status.Failure; // every child failed
}
}
```
The reciprocal `Sequence` (AND — stop at first non-`Success`), `Parallel`, the `Blackboard`, the
leaf base classes, and every decorator are in `references/behavior-tree-core.md`.
## Utility scoring in one snippet
```csharp
// A consideration maps one raw fact to 0..1 through a response curve.
float Score(Blackboard bb)
{
float distance01 = Curves.InverseLerp01(bb.Get<float>("distToPlayer"), 20f, 2f); // near = 1
float health01 = Curves.Sigmoid(bb.Get<float>("health01"), k: 8f, mid: 0.4f); // hurt = low
// Product + compensation keeps a single 0 from vetoing while low values still dampen.
return Curves.CompensatedProduct(new[] { distance01, health01 });
}
```
The full curve library (linear, quadratic, exponential, logistic/sigmoid, smoothstep), the
`Consideration`/`UtilityAction` types, and the `UtilityEvaluator` selection strategies are in
`references/utility-ai-system.md`.
## Pitfalls
- **Re-ticking a `Running` action from the root every frame restarts it.** Return `Running` and
resume where you left off; only `Reset()` a subtree when a parent actually abandons it.
- **Deep trees re-evaluated wholesale each tick** waste time and cause thrash. Prefer shallow
trees and *conditional aborts* (a higher-priority condition can interrupt a lower branch).
- **Un-normalized considerations.** If one curve outputs 0..100 and another 0..1, the big one
dominates. Every consideration must return 0..1.
- **Utility jitter on near-ties.** Add hysteresis: give the currently-running action a small bonus
so the agent commits instead of oscillating.
- **Allocating nodes, closures, or arrays every tick** creates GC spikes. Build the tree once at
spawn; keep per-tick work allocation-free.
## References
- `references/behavior-tree-core.md` — Blackboard, `Node`/leaf base classes, action & condition
leaves, `Sequence`/`Selector`/`Parallel`, and the decorator library (full C#).
- `references/utility-ai-system.md` — response-curve library, `Consideration`, `UtilityAction`,
and the `UtilityEvaluator` (argmax, softmax, weighted-random, hysteresis).
- `references/practical-examples.md` — a guard Patrol→Combat BT, a villager needs-based Utility
AI, and a hybrid agent, as drop-in templates.
- `references/best-practices-and-pitfalls.md` — memory management, profiling, avoiding deep trees,
event-driven aborts, and combining Utility AI with BTs (hybrid architecture).
## Related skills
- `game-ai` — choose between FSM / BT / steering; A* and navmesh pathfinding.
- `unreal-behavior-trees` — Unreal's asset-based BT/Blackboard, tasks, decorators, services.
- `unity-navmesh` — the `NavMeshAgent` that carries out "move to" intents.
- `physics-tuning` — agent radius, movement, and collision response for the motion layer.
- `tower-defense`, `fps-shooter`, `rpg` — genres that compose this decision layer.
@@ -0,0 +1,392 @@
# Behavior Tree core — Blackboard, nodes, leaves, composites, decorators
Depth for `ai-behavior-trees-utility-ai`. A complete, allocation-conscious C# behavior-tree
runtime. The code targets plain C# (no engine types) so it drops into Unity 6 or Godot 4 C#
unchanged — bind the leaves to your engine's transform/navigation in the concrete actions.
Contract in one line: every node's `Tick` returns `Success`, `Failure`, or `Running`, and a
parent decides what that means. Build the tree once at spawn; keep per-tick work allocation-free.
## Blackboard — the shared state system
The **Blackboard** is the agent's working memory. Leaves read and write it; no node holds a
reference to another. That indirection is what lets the same `MoveTo` action serve chase, patrol,
and flee subtrees, and what makes subtrees reusable across enemy types.
```csharp
using System.Collections.Generic;
// A typed key/value store. Keys are strings (or use enum/int keys to avoid hashing cost).
public sealed class Blackboard
{
private readonly Dictionary<string, object> _values = new();
public void Set<T>(string key, T value) => _values[key] = value!;
public T Get<T>(string key, T fallback = default!)
=> _values.TryGetValue(key, out var v) && v is T t ? t : fallback;
public bool TryGet<T>(string key, out T value)
{
if (_values.TryGetValue(key, out var v) && v is T t) { value = t; return true; }
value = default!;
return false;
}
public bool Has(string key) => _values.ContainsKey(key);
public void Remove(string key) => _values.Remove(key);
}
```
For hot agents, prefer a **struct-of-fields blackboard** (public fields on a class) over a string
dictionary — it removes hashing and boxing entirely. Use the dictionary form when designers add
keys at runtime or you serialize the board.
```csharp
// Zero-allocation alternative: a plain data object shared by every node on this agent.
public sealed class AgentContext
{
public Vector2 Position;
public Transform Target; // null when no target
public Vector2 Home;
public float LastSeenTime;
public readonly List<Vector2> Path = new();
}
```
## The node base and status
```csharp
public enum Status { Success, Failure, Running }
// Every node in the tree derives from Node. dt is the decision-step delta (may differ from frame dt).
public abstract class Node
{
public abstract Status Tick(Blackboard bb, float dt);
// Called when a parent stops running this node before it finished (aborted branch).
// Override to release timers, animations, or reservations.
public virtual void Reset() { }
}
```
`Reset()` is the half of the contract people forget. When a `Selector` switches from a running
low-priority branch to a higher-priority one, it must `Reset()` the abandoned branch so a
half-finished action (a playing attack animation, a reserved cover point) is cleaned up.
## Base classes: leaf, composite, decorator
Three structural node kinds cover every tree:
```csharp
// A leaf does the actual work; it has no children.
public abstract class Leaf : Node { }
// A composite has many children and defines how their statuses combine (see Composites below).
public abstract class Composite : Node
{
protected readonly List<Node> Children = new();
protected int Current; // resume index for Running composites
public Composite Add(Node child) { Children.Add(child); return this; }
public override void Reset()
{
Current = 0;
foreach (var c in Children) c.Reset();
}
}
// A decorator wraps exactly one child and transforms its status or gates it (see Decorators below).
public abstract class Decorator : Node
{
protected Node Child = default!;
public Decorator Wrap(Node child) { Child = child; return this; }
public override void Reset() => Child.Reset();
}
```
## Condition leaves — instantaneous predicates
A **condition** reads the blackboard and returns `Success` or `Failure` in the same tick. It never
returns `Running` and never mutates game state. Express reusable predicates as one small class with
an injected test, or subclass for named conditions.
```csharp
// Generic condition: succeed when a predicate over the blackboard holds.
public sealed class Condition : Leaf
{
private readonly System.Func<Blackboard, bool> _predicate;
public Condition(System.Func<Blackboard, bool> predicate) => _predicate = predicate;
public override Status Tick(Blackboard bb, float dt)
=> _predicate(bb) ? Status.Success : Status.Failure;
}
// Named condition when the check is non-trivial or reused across trees.
public sealed class CanSeeTarget : Leaf
{
private readonly float _sightRange;
public CanSeeTarget(float sightRange) => _sightRange = sightRange;
public override Status Tick(Blackboard bb, float dt)
{
if (!bb.TryGet<Vector2>("targetPos", out var target)) return Status.Failure;
var self = bb.Get<Vector2>("position");
// Real games also raycast for line of sight; keep the check cheap and cache the result.
return (target - self).sqrMagnitude <= _sightRange * _sightRange
? Status.Success : Status.Failure;
}
}
```
Note the pre-allocated `System.Func` delegate: create conditions **once** when the tree is built,
not per tick, so no closure is allocated during evaluation.
## Action leaves — multi-frame work that returns Running
An **action** performs work and returns `Running` until it completes, then `Success` (or `Failure`
if it can't). Returning `Running` is what lets a walk, an animation, or a timed wait span many
ticks without the parent restarting it.
```csharp
// Move toward a blackboard target; Running until within arrive radius, then Success.
public sealed class MoveToTarget : Leaf
{
private readonly float _speed, _arriveRadius;
public MoveToTarget(float speed, float arriveRadius)
{ _speed = speed; _arriveRadius = arriveRadius; }
public override Status Tick(Blackboard bb, float dt)
{
if (!bb.TryGet<Vector2>("targetPos", out var target)) return Status.Failure;
var pos = bb.Get<Vector2>("position");
var offset = target - pos;
if (offset.magnitude <= _arriveRadius) return Status.Success; // arrived
pos += offset.normalized * _speed * dt;
bb.Set("position", pos); // in an engine, drive the NavMeshAgent / CharacterBody here
return Status.Running; // keep going next tick
}
}
// A timed wait — the canonical Running action, useful for patrol pauses and cooldown holds.
public sealed class Wait : Leaf
{
private readonly float _duration;
private float _elapsed;
public Wait(float duration) => _duration = duration;
public override Status Tick(Blackboard bb, float dt)
{
_elapsed += dt;
if (_elapsed < _duration) return Status.Running;
return Status.Success;
}
public override void Reset() => _elapsed = 0f; // restart cleanly if the branch is re-entered
}
```
`Wait` shows why `Reset()` matters: its `_elapsed` accumulator must be cleared when the branch is
abandoned and later re-entered, or the second wait finishes instantly.
The `Sequence`/`Selector`/`Parallel` execution logic and the decorator library build directly on
these base classes — see the composites and decorators sections below.
## Composites — how child statuses combine
Composites are the control flow of a tree. The two you use constantly are `Sequence` (AND) and
`Selector` (OR/fallback); `Parallel` covers concurrent branches.
```csharp
// Sequence = AND: tick children in order; stop at the first that is not Success.
// Resumes at the Running child next tick ("memory" variant).
public sealed class Sequence : Composite
{
public override Status Tick(Blackboard bb, float dt)
{
for (; Current < Children.Count; Current++)
{
var s = Children[Current].Tick(bb, dt);
if (s == Status.Running) return Status.Running; // resume here next tick
if (s == Status.Failure) { Current = 0; return Status.Failure; }
}
Current = 0;
return Status.Success; // every child succeeded
}
}
// Selector = OR / fallback: return the first child that is not Failure.
public sealed class Selector : Composite
{
public override Status Tick(Blackboard bb, float dt)
{
for (; Current < Children.Count; Current++)
{
var s = Children[Current].Tick(bb, dt);
if (s == Status.Running) return Status.Running; // resume here next tick
if (s == Status.Success) { Current = 0; return Status.Success; }
}
Current = 0;
return Status.Failure; // every child failed
}
}
```
**Memory vs reactive.** The versions above *remember* the `Running` child and resume there. That is
efficient but does not let a higher-priority sibling interrupt. For a **reactive** selector — the
common case for combat AI — re-check children from index 0 every tick and abort the running branch
when an earlier child changes its mind:
```csharp
// Reactive selector: earlier (higher-priority) children can preempt a lower running branch.
public sealed class ReactiveSelector : Composite
{
private int _running = -1;
public override Status Tick(Blackboard bb, float dt)
{
for (int i = 0; i < Children.Count; i++)
{
var s = Children[i].Tick(bb, dt);
if (s == Status.Failure) continue;
if (_running != -1 && _running != i) Children[_running].Reset(); // abort old branch
_running = s == Status.Running ? i : -1;
return s; // Success or Running
}
_running = -1;
return Status.Failure;
}
public override void Reset() { base.Reset(); _running = -1; }
}
```
`Parallel` ticks every child each step and resolves with a policy — succeed when *any* (or *all*)
succeed, fail when *any* (or *all*) fail. Use it for "attack while strafing" or "play VFX while
moving".
```csharp
public enum ParallelPolicy { RequireOne, RequireAll }
public sealed class Parallel : Composite
{
private readonly ParallelPolicy _success, _failure;
public Parallel(ParallelPolicy success, ParallelPolicy failure)
{ _success = success; _failure = failure; }
public override Status Tick(Blackboard bb, float dt)
{
int successes = 0, failures = 0;
foreach (var child in Children)
{
var s = child.Tick(bb, dt);
if (s == Status.Success) successes++;
else if (s == Status.Failure) failures++;
}
if (_failure == ParallelPolicy.RequireOne && failures > 0) return Status.Failure;
if (_failure == ParallelPolicy.RequireAll && failures == Children.Count) return Status.Failure;
if (_success == ParallelPolicy.RequireOne && successes > 0) return Status.Success;
if (_success == ParallelPolicy.RequireAll && successes == Children.Count) return Status.Success;
return Status.Running;
}
}
```
## Decorators — wrap one child to change its meaning
Decorators add policy without new leaves: invert a result, force a status, gate on a cooldown, or
repeat. Each wraps exactly one child.
```csharp
// Inverter: Success <-> Failure (Running passes through). "NOT".
public sealed class Inverter : Decorator
{
public override Status Tick(Blackboard bb, float dt) => Child.Tick(bb, dt) switch
{
Status.Success => Status.Failure,
Status.Failure => Status.Success,
_ => Status.Running,
};
}
// ForceSuccess: swallow a child's failure so a Sequence keeps going (optional steps).
public sealed class ForceSuccess : Decorator
{
public override Status Tick(Blackboard bb, float dt)
=> Child.Tick(bb, dt) == Status.Running ? Status.Running : Status.Success;
}
// Repeat: run the child up to n times (n <= 0 = forever), restarting on each Success.
public sealed class Repeat : Decorator
{
private readonly int _count;
private int _done;
public Repeat(int count) => _count = count;
public override Status Tick(Blackboard bb, float dt)
{
var s = Child.Tick(bb, dt);
if (s == Status.Running) return Status.Running;
if (s == Status.Failure) { _done = 0; return Status.Failure; }
_done++;
if (_count > 0 && _done >= _count) { _done = 0; return Status.Success; }
Child.Reset(); // loop again
return Status.Running;
}
public override void Reset() { base.Reset(); _done = 0; }
}
// Cooldown: gate the child so it can only run once per interval (attack/ability rate-limiting).
public sealed class Cooldown : Decorator
{
private readonly float _seconds;
private float _readyAt;
public Cooldown(float seconds) => _seconds = seconds;
public override Status Tick(Blackboard bb, float dt)
{
float now = bb.Get<float>("time");
if (now < _readyAt) return Status.Failure; // still cooling down
var s = Child.Tick(bb, dt);
if (s == Status.Success) _readyAt = now + _seconds;
return s;
}
public override void Reset() { base.Reset(); _readyAt = 0f; }
}
```
## Running the tree
Wrap the root and tick it on your decision cadence. A behavior tree does **not** need to tick every
render frame — 515 Hz is plenty for most NPCs and slashes CPU cost.
```csharp
public sealed class BehaviorTree
{
private readonly Node _root;
private readonly Blackboard _bb;
public BehaviorTree(Node root, Blackboard bb) { _root = root; _bb = bb; }
public Status Tick(float dt)
{
_bb.Set("time", _bb.Get<float>("time") + dt); // keep a clock for Cooldown/Wait
return _root.Tick(_bb, dt);
}
}
// Fluent assembly — build once at spawn, then tick.
var tree = new BehaviorTree(
new ReactiveSelector()
.Add(new Sequence() // combat branch (highest priority)
.Add(new CanSeeTarget(sightRange: 12f))
.Add(new MoveToTarget(speed: 6f, arriveRadius: 1.5f))
.Add(new Cooldown(0.8f).Wrap(new AttackTarget())))
.Add(new Patrol(waypoints)), // fallback
blackboard);
```
The `Patrol` and `AttackTarget` leaves, and full agents that assemble these pieces, are in
`references/practical-examples.md`.
@@ -0,0 +1,92 @@
# Best practices & pitfalls — performance, memory, hybrid AI
Depth for `ai-behavior-trees-utility-ai`. How to keep a BT/Utility runtime fast, debuggable, and
maintainable at scale (dozenshundreds of agents), and how to combine the two models well.
## Memory management
- **Build the tree once, at spawn — never per tick.** Node objects, delegates, and consideration
lists are created during assembly and reused every tick. A tree rebuilt each frame is both a GC
storm and a correctness bug (it discards `Running` state).
- **Keep `Tick` allocation-free.** No `new`, no LINQ (`Where`/`Select` allocate iterators), no
closures capturing locals, no boxing. Pre-allocate scratch buffers; iterate with indexed `for`.
- **Prefer a struct-of-fields blackboard** (or `int`/enum keys) over a `Dictionary<string,object>`
on hot agents — it removes hashing and value-type boxing. Keep the dictionary form for
designer-authored or serialized boards.
- **Pool agents and their trees.** Reuse a despawned enemy's tree instance on respawn; call
`Reset()` instead of reallocating.
- **Share immutable data.** Curves and static config are stateless — one instance serves every
agent. Only per-agent mutable state (the blackboard, `Running` indices) is unique.
## Performance & profiling
- **Profile before optimizing.** Measure AI time in the engine profiler (Unity Profiler, Godot
Monitors, Unreal `stat game`). The usual cost is not the tree walk — it is what the *leaves* do
(raycasts, pathfinding, `FindObjectsByType`). Cache perception; pathfind on a timer, not per tick.
- **Tick on a decision cadence, not per frame.** 515 Hz is imperceptible for most NPCs and cuts
cost 410×. Drive the tree from an accumulator (see the guard in `practical-examples.md`).
- **Time-slice across frames.** Don't tick every agent on the same frame. Stagger by bucketing
agents and ticking one bucket per frame, so the cost spreads instead of spiking.
```csharp
// Round-robin: only ~1/N of agents think each frame; the herd cost is flat, not a spike.
_bucket = (_bucket + 1) % Buckets;
for (int i = _bucket; i < agents.Count; i += Buckets)
agents[i].Think(dt * Buckets); // scale dt so per-agent cadence is unchanged
```
- **LOD your AI.** Distant or off-screen agents tick slower (or freeze). Tie the cadence to
distance-from-camera; a guard 200 m away does not need 10 Hz decisions.
- **Utility cost scales with actions × considerations.** Don't score 40 actions × 8 considerations
every tick. Prune obviously-irrelevant actions first (a cheap gate consideration that early-outs
at 0), and re-score only when a relevant fact changed.
## Avoiding deep, brittle trees
- **Keep trees shallow and wide.** Deep nesting is hard to read and forces full re-evaluation.
Factor repeated subtrees into named builder methods and reuse them.
- **Use conditional aborts / a reactive selector** so a high-priority condition (took damage, lost
the player) interrupts a lower branch, instead of polling a deep tree for the change.
- **Prefer event-driven perception over polling.** Let sensors push `targetPos` onto the blackboard
when they fire; the tree reads a cached fact instead of raycasting inside a condition every tick.
- **Cache condition results within a tick** if the same expensive check appears in multiple places.
## Combining Utility AI with behavior trees (hybrid)
The two models answer different questions — use each where it is strong:
| Use a **behavior tree** for… | Use **Utility AI** for… |
|---|---|
| Top-level structure & priorities (patrol / engage / flee) | "How much do I want each option right now?" |
| Ordered, interruptible sequences | Target selection, item/ability choice, needs |
| Clear, debuggable, designer-readable flow | Smooth trade-offs with many inputs |
Recommended default: **BT on the outside, Utility on the inside.** The BT decides *engage vs
disengage vs patrol*; a `UtilitySelectorNode` inside the engage branch decides *which target / which
attack* (see `practical-examples.md` §3). Keep the utility set small and local to the branch so
scoring stays cheap, and give the running action hysteresis so the sub-choice doesn't flicker.
Avoid the inverse (utility choosing between whole behavior trees) unless you truly need graded
top-level behavior — it is harder to debug and easy to make thrash.
## Debugging
- **Draw the decision.** Overlay the active BT path (highlight the running leaf) and, for utility,
a live bar per action score. Most "bad AI" bugs are visible instantly: a stuck `Running` leaf, an
un-normalized consideration pinning one action to 1.0, or a mis-shaped curve.
- **Log transitions, not ticks.** Print only when the chosen action or active branch *changes*;
per-tick logs bury the signal.
- **Make randomness reproducible.** Seed the `System.Random` used by softmax/weighted-random per
agent so a misbehaving agent can be replayed. Never use a shared global RNG across agents.
- **Assert the `Reset()` contract.** A common bug is a `Running` action (`Wait`, `Repeat`, cover
reservation) that isn't reset when its branch is abandoned. If timed actions "finish instantly"
after re-entry, a missing `Reset()` is the cause.
## Pitfall quick-reference
- Rebuilding the tree or allocating in `Tick` → GC spikes and lost `Running` state.
- Ticking every agent every frame → CPU spikes; use cadence + time-slicing + LOD.
- Un-normalized considerations (mixed 0..1 and 0..100) → one factor dominates; weights meaningless.
- No hysteresis → jitter on near-ties in both reactive selectors and utility selection.
- Deep trees with polling conditions → wasted work and laggy reactions; use aborts + events.
- Expensive work inside conditions (raycasts, pathfinding) → cache it on the blackboard instead.
@@ -0,0 +1,181 @@
# Practical examples — guard AI, villager needs, hybrid agent
Depth for `ai-behavior-trees-utility-ai`. Three drop-in templates that assemble the BT core
(`references/behavior-tree-core.md`) and the Utility system (`references/utility-ai-system.md`) into
real agents. Same plain-C# style — bind the movement/attack leaves to your engine's transform,
navigation, and combat.
## 1. Enemy guard — Patrol → Combat behavior tree
The classic guard: patrol waypoints until it sees the player, then close in and attack on a
cooldown, and fall back to patrol when the player escapes. Two extra leaves complete the set the
core promised:
```csharp
// Patrol: walk a waypoint ring, pausing at each. Always Running (a patrol never "finishes").
public sealed class Patrol : Leaf
{
private readonly Vector2[] _points;
private readonly float _speed, _pause;
private int _i;
private float _waited;
public Patrol(Vector2[] points, float speed = 3f, float pause = 1f)
{ _points = points; _speed = speed; _pause = pause; }
public override Status Tick(Blackboard bb, float dt)
{
var pos = bb.Get<Vector2>("position");
var goal = _points[_i];
if ((goal - pos).magnitude > 0.2f)
{
bb.Set("position", pos + (goal - pos).normalized * _speed * dt);
return Status.Running;
}
_waited += dt; // reached a waypoint: pause, then advance
if (_waited >= _pause) { _waited = 0f; _i = (_i + 1) % _points.Length; }
return Status.Running;
}
public override void Reset() => _waited = 0f;
}
// AttackTarget: one swing. Wrap in a Cooldown decorator to rate-limit (see the assembly below).
public sealed class AttackTarget : Leaf
{
public override Status Tick(Blackboard bb, float dt)
{
if (!bb.TryGet<IDamageable>("target", out var target)) return Status.Failure;
target.ApplyDamage(10f);
return Status.Success;
}
}
```
Assemble and drive it. Perception writes `targetPos`/`target` onto the blackboard; the tree only
*decides*:
```csharp
public sealed class GuardAgent
{
private readonly BehaviorTree _tree;
private readonly Blackboard _bb = new();
private float _decisionTimer;
private const float TickRate = 1f / 10f; // decide at 10 Hz, not every frame
public GuardAgent(Vector2[] patrolRoute)
{
_tree = new BehaviorTree(
new ReactiveSelector() // reactive: seeing the player preempts patrol
.Add(new Sequence() // --- combat branch (higher priority) ---
.Add(new CanSeeTarget(sightRange: 12f))
.Add(new MoveToTarget(speed: 6f, arriveRadius: 1.5f))
.Add(new Cooldown(0.8f).Wrap(new AttackTarget())))
.Add(new Patrol(patrolRoute)), // --- fallback ---
_bb);
}
// Call every frame; the tree itself only ticks on the decision cadence.
public void Update(float dt, PerceptionResult perception)
{
_bb.Set("position", perception.SelfPos);
if (perception.SeesPlayer) { _bb.Set("targetPos", perception.PlayerPos); _bb.Set("target", perception.Player); }
else _bb.Remove("targetPos");
_decisionTimer += dt;
if (_decisionTimer < TickRate) return;
_tree.Tick(_decisionTimer);
_decisionTimer = 0f;
}
}
```
## 2. Villager needs — Utility AI
When behavior is driven by competing *needs* rather than a fixed priority order, Utility AI is the
better fit. A villager weighs hunger, fatigue, and social need against opportunity, and does the
most-wanted thing:
```csharp
UtilityEvaluator BuildVillagerBrain()
{
// Each action: weight * combined considerations (all 0..1). Curves shape the "want".
var eat = new UtilityAction("Eat") { Weight = 1.2f }
.With(new Consideration("hunger", bb => bb.Get<float>("hunger01"),
t => Curves.Sigmoid(t, k: 10f, mid: 0.5f))) // want food as hunger rises
.With(new Consideration("hasFood", bb => bb.Get<float>("food01"), Curves.Linear));
var sleep = new UtilityAction("Sleep") { Weight = 1f }
.With(new Consideration("fatigue", bb => bb.Get<float>("fatigue01"),
t => Curves.Quadratic(t))) // only strong when very tired
.With(new Consideration("isNight", bb => bb.Get<float>("night01"), Curves.Smoothstep));
var socialize = new UtilityAction("Socialize") { Weight = 0.8f }
.With(new Consideration("lonely", bb => bb.Get<float>("loneliness01"), Curves.Linear))
.With(new Consideration("friendsNear", bb => bb.Get<float>("friendsNear01"), Curves.Smoothstep));
var work = new UtilityAction("Work") { Weight = 0.7f }
.With(new Consideration("daytime", bb => bb.Get<float>("day01"), Curves.Smoothstep))
.With(new Consideration("notTooTired",
bb => 1f - bb.Get<float>("fatigue01"), Curves.Linear)); // inverted fact
return new UtilityEvaluator().Add(eat).Add(sleep).Add(socialize).Add(work);
}
// Per decision step: pick and dispatch. Hysteresis stops the villager thrashing between near-ties.
void UpdateVillager(UtilityEvaluator brain, Blackboard bb)
{
var choice = brain.SelectBest(bb, inertiaBonus: 0.05f);
Dispatch(choice.Name); // route "Eat"/"Sleep"/... to your gameplay handlers
}
```
## 3. Hybrid — a BT that delegates a choice to Utility AI
The best of both: a behavior tree gives the top-level *structure and priorities*; a utility node
makes a *graded sub-choice* (which target, which attack) inside a branch. Bridge them with a leaf
that owns a `UtilityEvaluator` and runs the winner's `Behavior` subtree.
```csharp
// A BT leaf that scores utility actions and ticks the chosen action's Behavior subtree.
public sealed class UtilitySelectorNode : Leaf
{
private readonly UtilityEvaluator _evaluator;
private readonly float _inertia;
private UtilityAction _running;
public UtilitySelectorNode(UtilityEvaluator evaluator, float inertia = 0.05f)
{ _evaluator = evaluator; _inertia = inertia; }
public override Status Tick(Blackboard bb, float dt)
{
var choice = _evaluator.SelectBest(bb, _inertia);
if (choice != _running) { _running?.Behavior?.Reset(); _running = choice; } // switched: clean up
return _running?.Behavior?.Tick(bb, dt) ?? Status.Failure;
}
public override void Reset() { _running?.Behavior?.Reset(); _running = null; }
}
```
```csharp
// Combat structured by a BT; "which attack" chosen by utility each tick.
var attackChoice = new UtilityEvaluator()
.Add(new UtilityAction("Melee") { Weight = 1f, Behavior = new MeleeCombo() }
.With(new Consideration("close", bb => Curves.InverseLerp01(bb.Get<float>("distToPlayer"), 6f, 1f), Curves.Smoothstep)))
.Add(new UtilityAction("Ranged") { Weight = 1f, Behavior = new FireVolley() }
.With(new Consideration("far", bb => Curves.InverseLerp01(bb.Get<float>("distToPlayer"), 2f, 14f), Curves.Smoothstep))
.With(new Consideration("ammo", bb => bb.Get<float>("ammo01"), Curves.Linear)));
var hybrid = new BehaviorTree(
new ReactiveSelector()
.Add(new Sequence()
.Add(new CanSeeTarget(12f))
.Add(new UtilitySelectorNode(attackChoice))) // BT picks "engage"; utility picks how
.Add(new Patrol(route)),
blackboard);
```
This layering is the recommended default for combat AI: keep the readable, debuggable BT for
"engage vs disengage vs patrol", and let utility handle the continuous trade-offs. The performance
and architecture trade-offs of the hybrid are in `references/best-practices-and-pitfalls.md`.
@@ -0,0 +1,174 @@
# Utility AI system — curves, considerations, evaluator
Depth for `ai-behavior-trees-utility-ai`. Utility AI turns *"which action do I want most right
now?"* into arithmetic: describe each candidate action with a few **considerations**, map each raw
fact through a **normalized response curve** to 0..1, combine them into a score, and select. Same
C#-only style as the BT core, so it composes with it (see the hybrid section in
`references/practical-examples.md`).
Golden rule: **every consideration outputs 0..1.** Mixing ranges (one 0..1, one 0..100) lets the
big one dominate and makes weights meaningless.
## Response-curve library
Curves shape *how* a fact translates to desire. `t` is a normalized 0..1 input (use
`InverseLerp01` to normalize a raw fact first). All outputs are clamped to 0..1.
```csharp
public static class Curves
{
public static float Clamp01(float x) => x < 0f ? 0f : x > 1f ? 1f : x;
// Normalize a raw fact into 0..1 (a==0 output, b==1 output). Handles inverted ranges (a>b).
public static float InverseLerp01(float x, float a, float b)
=> a == b ? (x >= b ? 1f : 0f) : Clamp01((x - a) / (b - a));
// Linear: desire tracks the fact directly.
public static float Linear(float t, float slope = 1f, float yIntercept = 0f)
=> Clamp01(slope * t + yIntercept);
// Polynomial: exponent>1 = ease-in (low until high t); exponent<1 = ease-out (rises fast early).
public static float Polynomial(float t, float exponent)
=> Clamp01((float)System.Math.Pow(Clamp01(t), exponent));
public static float Quadratic(float t) => Polynomial(t, 2f); // slow start, sharp finish
public static float InverseQuadratic(float t) // fast start, gentle finish
=> Clamp01(1f - (1f - Clamp01(t)) * (1f - Clamp01(t)));
// Exponential: k>0 grows steeply near 1; useful for "only care when nearly full/empty".
public static float Exponential(float t, float k = 4f)
=> Clamp01(((float)System.Math.Exp(k * Clamp01(t)) - 1f) / (System.MathF.Exp(k) - 1f));
// Logistic / sigmoid: a smooth S-curve with an adjustable threshold. k = steepness, mid = 0.5 crossing.
public static float Sigmoid(float t, float k = 8f, float mid = 0.5f)
=> Clamp01(1f / (1f + System.MathF.Exp(-k * (Clamp01(t) - mid))));
// Smoothstep: eased 0..1 with zero slope at both ends; good default for "soft threshold".
public static float Smoothstep(float t) { t = Clamp01(t); return t * t * (3f - 2f * t); }
}
```
Pick by intent: **Linear** for proportional wants; **Quadratic/Exponential** for "only matters at
the extreme"; **Sigmoid/Smoothstep** for a soft threshold (flee when health crosses ~40%); invert
any curve with `1 - curve(t)` (e.g. desire-to-heal rises as health falls).
## Considerations — one fact, one curve
A **consideration** binds a fact reader to a curve and yields a 0..1 factor. Build them once and
reuse; they hold no per-tick state.
```csharp
public sealed class Consideration
{
public readonly string Name;
private readonly System.Func<Blackboard, float> _fact; // raw or pre-normalized fact
private readonly System.Func<float, float> _curve; // maps the fact to 0..1
public Consideration(string name, System.Func<Blackboard, float> fact,
System.Func<float, float> curve)
{ Name = name; _fact = fact; _curve = curve; }
public float Evaluate(Blackboard bb) => Curves.Clamp01(_curve(_fact(bb)));
}
```
```csharp
// Example considerations for a "attack the player" action.
var proximity = new Consideration("proximity",
bb => Curves.InverseLerp01(bb.Get<float>("distToPlayer"), 20f, 2f), // near -> 1
Curves.Smoothstep);
var myHealth = new Consideration("myHealth",
bb => bb.Get<float>("health01"),
t => Curves.Sigmoid(t, k: 8f, mid: 0.35f)); // low health -> low desire
```
## Scored actions and the compensation factor
A `UtilityAction` scores as its `weight` times the **combined** considerations. Multiplying factors
is standard (a single near-zero consideration should veto the action), but naive multiplication
over-punishes as you add considerations. Apply Dave Mark's **compensation factor** so more
considerations don't unfairly drag the score down.
```csharp
public sealed class UtilityAction
{
public readonly string Name;
public float Weight = 1f;
public readonly List<Consideration> Considerations = new();
public Node Behavior; // optional BT subtree to run when chosen (hybrid)
public UtilityAction(string name) => Name = name;
public UtilityAction With(Consideration c) { Considerations.Add(c); return this; }
public float Score(Blackboard bb)
{
int n = Considerations.Count;
if (n == 0) return Weight;
float product = 1f;
for (int i = 0; i < n; i++) product *= Considerations[i].Evaluate(bb);
// Compensation: add back some of what extra factors removed. modFactor -> 0 as n grows.
float modFactor = 1f - 1f / n;
float makeUp = (1f - product) * modFactor;
float compensated = product + makeUp * product;
return Weight * Curves.Clamp01(compensated);
}
}
```
## The evaluator — scoring and selection
The evaluator scores every action and selects one. Offer three strategies: **argmax** (best),
**softmax** (probabilistic, weighted by score — variety without dumb choices), and
**weighted-random** over raw scores. Add **hysteresis** (an inertia bonus for the current action)
so agents commit instead of flip-flopping on near-ties.
```csharp
public sealed class UtilityEvaluator
{
private readonly List<UtilityAction> _actions = new();
private UtilityAction _current;
public UtilityEvaluator Add(UtilityAction a) { _actions.Add(a); return this; }
// Argmax with hysteresis: the currently-committed action gets a small bonus.
public UtilityAction SelectBest(Blackboard bb, float inertiaBonus = 0.05f)
{
UtilityAction best = null; float bestScore = float.NegativeInfinity;
foreach (var a in _actions)
{
float s = a.Score(bb);
if (a == _current) s += inertiaBonus; // stickiness to avoid jitter
if (s > bestScore) { bestScore = s; best = a; }
}
_current = best;
return best;
}
// Softmax: choose proportionally to exp(score/temperature). Higher temp = more random.
public UtilityAction SelectSoftmax(Blackboard bb, System.Random rng, float temperature = 0.2f)
{
float sum = 0f;
var weights = new float[_actions.Count];
for (int i = 0; i < _actions.Count; i++)
{
weights[i] = System.MathF.Exp(_actions[i].Score(bb) / System.MathF.Max(temperature, 1e-4f));
sum += weights[i];
}
float roll = (float)rng.NextDouble() * sum;
for (int i = 0; i < weights.Length; i++)
if ((roll -= weights[i]) <= 0f) { _current = _actions[i]; return _actions[i]; }
_current = _actions[^1];
return _current;
}
}
```
Usage: score every decision step, then run the winner's `Behavior` (hybrid) or call its handler
directly. Verify by drawing each action's live score as a debug bar while tuning — utility bugs are
almost always a mis-shaped curve or an un-normalized fact, and both are obvious on screen. The
worked villager-needs evaluator and the BT+Utility hybrid are in
`references/practical-examples.md`.