miaoda-game-behavior-core
v0.3.0
Published
Engine-independent behavior trees with perception, steering, deterministic RNG, bounded diagnostics, and explicitly invalidated path-replanning caches.
Downloads
556
Maintainers
Readme
miaoda-game-behavior-core
Use this engine-independent behavior-tree layer for guards, enemies, companions, and NPCs. It provides line-of-sight/FOV conditions and steering actions on top of mistreevous; your game supplies the entity bridge, custom actions, and optional path planner.
Install
pnpm add miaoda-game-behavior-core mistreevous miaoda-game-vision2d-coreMinimal tree
import { BehaviorRunner, State, type BehaviorAgent } from 'miaoda-game-behavior-core';
const definition = `root {
selector {
sequence { condition [CanSeeTarget] action [MoveToTarget] }
action [Patrol]
}
}`;
let dt = 0;
const agent: BehaviorAgent = {
config: { speed: 80, sightRange: 400, fovDeg: 70 },
getPosition: () => enemyPosition,
setPosition: (position) => { enemyPosition = position; },
getFacingDeg: () => facingDegrees,
getTarget: () => playerPosition,
getObstacles: () => visionObstacles,
getDeltaTime: () => dt,
};
const runner = new BehaviorRunner(definition, agent, {
Patrol: (a) => { patrol(a as BehaviorAgent); return State.SUCCEEDED; },
});
function tick(seconds: number) {
dt = seconds;
runner.step(seconds);
}step takes finite seconds and should run once per simulation tick. CanSeeTarget uses distance, shared obstacles, and optional facing cone. MoveToTarget and MoveTo return RUNNING until within arriveRadius, then SUCCEEDED; they never overshoot.
Built-in leaves
| Tree expression | Purpose |
| --- | --- |
| condition [CanSeeTarget] | Line of sight and optional FOV |
| condition [IsTargetInRange radius] | Straight-line range check |
| action [MoveToTarget] | Steer directly toward current target |
| action [MoveToTargetAvoiding] | Follow agent.findPath when available |
| action [MoveTo x y] | Steer toward a fixed point |
| action [FleeFromTarget] | Steer away while a target exists |
followPath(agent, waypoints) is exported for custom patrol or route leaves. Without findPath, direct steering treats obstacles as sight blockers only; it does not navigate around walls.
Agent bridge
Implement BehaviorAgent with position read/write, facing in degrees (0° = +X), target position, Obstacles, and the current delta in seconds. config.speed is world units per second; arriveRadius, sightRange, and fovDeg tune movement and perception. Use the same Obstacles instance that drives your vision rendering so AI and player-facing cones agree.
For terrain navigation, add findPath(from, to): Vec2[] | null and use MoveToTargetAvoiding. The planner can adapt miaoda-game-grid-core A* or a navmesh; the behavior package consumes the route but does not own the algorithm.
Cached replanning
The compatibility function moveToTargetAvoiding(agent) plans every call. For expensive planners, give each agent a ReplanningPathFollower with explicit invalidation rules:
const navigator = new ReplanningPathFollower({
maxAgeSeconds: 0.3,
targetMoveDistance: 40,
unreachableRetrySeconds: 0.6,
getNavigationRevision: () => navigationRevision,
onReplan: (event) => aiTrace.record(event),
});
navigator.step(agent);navigationRevision remains host-owned: increment it when doors, blockers, traversal costs, or a navmesh change. The follower owns only copied waypoints and simulation-time cache age. It never queries physics or integrates position itself.
For MDSL trees, set BehaviorRunnerOptions.pathReplanning to the same options and the built-in action [MoveToTargetAvoiding] uses one follower owned by that runner. Omit it to preserve plan-every-call behavior.
Diagnostics and deterministic trees
Pass runner options as the fourth constructor argument. A seeded or authoritative random source controls lotto and ranged wait/repeat/retry nodes. Diagnostic history is opt-in and bounded; traceCapacity: 0 (the default) retains nothing.
const runner = new BehaviorRunner(definition, agent, customFunctions, {
random: seededRandom,
diagnostics: {
traceCapacity: 128,
onNodeStateChange: (change) => debugPanel.record(change),
},
});
const snapshot = runner.getSnapshot();
const recent = runner.getRecentTransitions();getSnapshot() returns a detached, recursively frozen id/type/name/state/children tree. Transitions include monotonic sequence and runner step numbers. Call clearTransitions() when a debug capture has been consumed. Observers run synchronously, so defer file, network, or cross-thread work to the host.
Public API
BehaviorRunner, BehaviorRunnerOptions, diagnostic snapshot/transition types, ReplanningPathFollower, replanning event/options types, State, canSeeTarget, isTargetWithin, moveToTarget, moveToTargetAvoiding, moveTo, fleeFromTarget, followPath, BehaviorAgent, SteeringConfig, and Vec2 are exported.
The core does not own a scene node, input devices, rendering, collision queries, or game-specific actions such as Attack and Patrol; provide those through the agent and custom functions.
