Skip to content

Latest commit

 

History

History
323 lines (262 loc) · 14.9 KB

File metadata and controls

323 lines (262 loc) · 14.9 KB

React Debugger

@bt-studio/react provides <BehaviourTreeDebugger>, a React component for visualizing and debugging behaviour trees. It uses React Flow for the tree graph, wraps TreeInspector from @bt-studio/core/inspector internally, and provides real-time plus time-travel debugging.

Installation

npm install @bt-studio/react @bt-studio/core react react-dom

Peer dependencies: react >= 18, react-dom >= 18, @bt-studio/core.

Quick Start

import { BehaviourTree, NodeResult } from '@bt-studio/core';
import { sequence, action, condition } from '@bt-studio/core/builder';
import { BehaviourTreeDebugger } from '@bt-studio/react';
import type { TickRecord } from '@bt-studio/core';

// Build and configure tree
const root = sequence({ name: 'Main' }, [
  condition({ name: 'Ready?', eval: () => true }),
  action({ name: 'Work', execute: () => NodeResult.Running }),
]);
const tree = new BehaviourTree(root).enableStateTrace();

// Collect tick records
const ticks: TickRecord[] = [];
for (let i = 0; i < 50; i++) {
  ticks.push(tree.tick({ now: i * 100 }));
}

// Render debugger
function App() {
  return (
    <BehaviourTreeDebugger
      tree={tree.serialize()}
      ticks={ticks}
      width="100%"
      height="600px"
    />
  );
}

Component API

<BehaviourTreeDebugger
  tree={serializedTree}           // SerializableNode from tree.serialize()
  ticks={tickRecords}             // TickRecord[] — append-only, component diffs internally
  inspectorOptions={{ maxTicks: 500 }}  // optional TreeInspector config
  inspectorRef={inspectorRef}     // optional escape-hatch to internal TreeInspector
  panels={{ nodeDetails: true, timeline: true, refTraces: true, activityNow: true }}
  activityDisplayMode="running_or_success" // "running" | "running_or_success" | "all"
  theme={{ colorSucceeded: '#22c55e' }}
  themeMode="dark"               // controlled: "dark" | "light"
  defaultThemeMode="dark"        // uncontrolled initial mode
  showToolbar={true}              // default true: top toolbar
  toolbarActions={<button type="button">Export</button>}
  showThemeToggle={true}          // default true: shows top-right moon/sun toggle
  onThemeModeChange={(mode) => {}}
  layoutDirection="TB"            // "TB" | "LR"
  width="100%" height="100%"
  isolateStyles={true}            // default true: render inside Shadow DOM
  onNodeSelect={(nodeId) => {}}
  onTickChange={(tickId) => {}}
  className="my-debugger"
/>

Props

Prop Type Default Description
tree SerializableNode required Serialized tree structure from tree.serialize()
ticks TickRecord[] required Append-only array of tick records. The component diffs internally to ingest only new ticks.
inspectorOptions TreeInspectorOptions {} Options passed to the internal TreeInspector (e.g., { maxTicks: 500 })
inspectorRef MutableRefObject<TreeInspector | null> Escape-hatch ref to access the internal TreeInspector for advanced queries
panels PanelConfig { nodeDetails: true, timeline: true, refTraces: true, activityNow: true } Toggle which panels are visible
activityDisplayMode "running" | "running_or_success" | "all" "running" Controls which activity branch results appear in the floating "Current Activity" panel
theme ThemeOverrides Partial token overrides for the active theme mode
themeMode "light" | "dark" Controlled color mode. When provided, parent controls mode state.
defaultThemeMode "light" | "dark" "dark" Initial mode for uncontrolled usage
onThemeModeChange (mode: "light" | "dark") => void Called when user toggles theme mode
showToolbar boolean true Shows the top toolbar area
toolbarActions ReactNode Custom action buttons/content rendered on the left side of the toolbar
showThemeToggle boolean true Shows the top-right moon/sun mode toggle in the toolbar
layoutDirection "TB" | "LR" "TB" Tree layout direction: top-to-bottom or left-to-right
width string | number "100%" Container width
height string | number "100%" Container height
isolateStyles boolean true Enables Shadow DOM style isolation so host-page CSS cannot bleed into the debugger
onNodeSelect (nodeId: number | null) => void Callback when a node is clicked
onTickChange (tickId: number) => void Callback when the viewed tick changes (scrubber, step, etc.)
className string Additional CSS class on the root element

Style Isolation

By default, the debugger renders inside a Shadow DOM root (isolateStyles={true}), which keeps host-page CSS from affecting internal debugger UI styles.

  • Host app styles do not cascade into the debugger internals.
  • className applies to the outer host container (useful for sizing/layout).
  • Set isolateStyles={false} if you need legacy, non-shadow rendering.

Transport

The component is transport-agnostic. You are responsible for collecting TickRecord objects and appending them to the ticks array. Examples:

Direct (same process):

const [ticks, setTicks] = useState<TickRecord[]>([]);

function gameLoop() {
  const record = tree.tick({ now: performance.now() });
  setTicks(prev => [...prev, record]);
  requestAnimationFrame(gameLoop);
}

WebSocket:

useEffect(() => {
  const ws = new WebSocket('ws://localhost:3001/ticks');
  ws.onmessage = (e) => {
    const record: TickRecord = JSON.parse(e.data);
    setTicks(prev => [...prev, record]);
  };
  return () => ws.close();
}, []);

Layout

The component uses a CSS grid layout:

  • Top toolbar (44px) — Action area (left), built-in center-tree button, and theme toggle (right)
  • With sidebar enabled, toolbar controls are split into canvas and sidebar tracks; Pause/Live is anchored to the sidebar-width track above node details
  • Canvas — Main area with the React Flow tree graph
  • Right sidebar (300px, collapsible) — Node details and ref details (tabbed)
  • Bottom bar (80px) — Timeline scrubber and playback controls

The tree is laid out using @dagrejs/dagre. Layout is recomputed only when the tree prop changes, not on every tick. The toolbar center-tree button recenters the viewport using fitView() without changing current tick selection.

Time-Travel

Two modes: Live and Paused.

  • Live: Automatically follows the latest tick. New ticks are displayed in real-time.
  • Paused: Frozen at a specific tick. Entering paused mode captures a frozen inspector snapshot so the viewed timeline/state does not shift while live ticks continue to arrive.
  • Percentiles in profiling UI are marked Approx in live mode (sampled for responsiveness), and become exact over the stored window when paused/time-travel is active.

Controls:

  • Toolbar Pause/Play: Toggle between live mode and paused time-travel mode directly from the top toolbar
  • Scrubber: Drag to jump to any stored tick (enters paused mode)
  • Step back/forward: Navigate one tick at a time
  • Left/Right arrows: ArrowLeft always steps backward globally (and from live mode enters paused time-travel), ArrowRight steps forward while paused; both support key-repeat while held and are ignored while typing in inputs/editors
  • Exit Time Travel / LIVE: Jump back to live mode
  • Esc key: Quick-exit paused mode and return to live

The toolbar and timeline also show the current tick's time value (TickRecord.timestamp):

  • If values look like Unix time (seconds or milliseconds), they are rendered as hh:mm:ss.
  • Otherwise they are shown as raw numeric values (for logical/monotonic clocks like game ticks).
  • Unit detection is inferred once and cached for subsequent ticks.
  • A toolbar toggle lets users switch between numeric and timestamp display at any time.

New ticks are always ingested by the live inspector even when paused, so no data is lost.

Result semantics are strict per tick:

  • If a node has no trace event at the viewed tick, it is treated as not ticked for that tick.
  • Display state may still show the latest known state at-or-before the viewed tick, and stale values are visually dimmed.

Node Visualization

Each node shows:

  • Display name with compact semantic badges
  • Utility, memory, and async nodes with dedicated letter badges (U, M, A), plus guard decorators with a compact condition-diamond badge icon
  • Memory composites surface active child index in the node name (for example MemorySequence (0)) and omit runningChildIndex from state rows
  • Type glyphs inspired by common behavior-tree notation conventions:
    • Sequence: single arrow (->) for ordered progression
    • Fallback/Selector: question mark (?) for "try alternatives"
    • Parallel: stacked lanes for concurrent child ticks
    • Condition: decision diamond with check
    • Action: task card/checklist mark
  • Optional compact decorator stack (non-lifecycle decorators) rendered above the decorated node (no connecting edge)
  • Lifecycle decorators collapsed to an inline thunder badge (⚡N) with hover names and click-to-cycle selection
  • Left accent stripe colored by result: green (Succeeded), red (Failed), amber (Running), gray (Idle)
  • Display state key-value pairs when present (from getDisplayState()), with dimmed stale rendering and auto-sizing content
  • Utility composites keep score state internal to feed child utility decorators in the UI; parent utility composites do not render the raw lastScores state rows
  • Ref changes for the viewed tick directly under the node/decorator that emitted them

Raw NodeFlags and numeric IDs are not shown in the tree canvas. Full flags remain visible in the node details panel.

Edges are smooth-step curves colored by child result when active, with animated dashes for Running children. The built-in minimap mirrors node positions and result colors, and supports pan/zoom interactions.

Node Details Panel

When a node is selected, the right sidebar shows:

  • Node header: Name, path, flags, tags, current result
  • Result distribution: Bar chart showing Succeeded/Failed/Running counts
  • Display state: Current key-value pairs from the node's display state
  • Tick history: Scrollable list of recent tick events with result dots (click to time-travel)

UX behavior:

  • Clicking a node in the graph switches to the Node Details tab
  • Programmatic node selection from ref timeline navigation does not force a tab switch

Tick history navigation supports keyboard and incremental loading:

  • Focus the history list and use ArrowUp/ArrowDown to move between ticks.
  • As you scroll down, older entries are loaded progressively (infinite-feed style).

Ref Details Panel

The "Ref Details" tab combines current ref state and historical event exploration:

  • Last known ref states: one row per ref with the latest value, the tick where it last changed, and a stale/fresh dot relative to the viewed tick
  • Ref event timeline: all stored RefChangeEvent entries across ticks, newest first, with ref name, tick, actor node badge, async badge, and value
  • Filters: narrow the timeline to a single ref name or switch scope to "Current tick" to see only mutations for the viewed tick
  • Click to time-travel: selecting either a latest-state row or a timeline event jumps to that tick, selects the actor node, and moves the canvas camera to it when present (without forcing a tab switch)

Node-attributed ref changes are shown directly below each node/decorator in the graph.

Current Activity Panel

The debugger renders a compact floating "Current Activity" panel over the canvas (similar to minimap placement). It is derived from per-tick events and node activity metadata.

  • One row per active branch (parallel branches produce multiple rows)
  • Label path is rendered from branch labels (for example Hunting > Movement > Kiting)
  • Rows are anchored to the branch tail activity node (last node in the active path that defines activity)
  • Click a row to focus/select its tail activity node and highlight the graph path from root to that tail
  • Use the in-window mode control (R, R+S, All) to switch whether only Running, Running + Succeeded, or all branch outcomes are shown
  • Use the in-window label source control (Activity / Node) to switch row text between activity labels and node names (name || defaultName)
  • The panel can be shown/hidden from the toolbar, dragged around the canvas, and collapsed to a single-line summary
  • The options strip inside the window can be collapsed independently via the header options toggle

Theming

The debugger now ships with minimalist, shadcn-style light and dark themes, with a built-in moon/sun toggle in the top toolbar.

  • Dark mode defaults to a VSCode-like charcoal palette.
  • Light mode uses subtle grays, clean borders, and low-contrast surfaces.
  • Pass theme to override any token while keeping the base mode.

Use controlled mode when you want to sync theme with your app:

const [mode, setMode] = useState<'light' | 'dark'>('dark');

<BehaviourTreeDebugger
  themeMode={mode}
  onThemeModeChange={setMode}
  // ...
/>

Use token overrides to customize either mode:

<BehaviourTreeDebugger
  theme={{
    colorSucceeded: '#22c55e',
    colorFailed: '#f14c4c',
    colorRunning: '#cca700',
    bgPrimary: '#1e1e1e',
    bgSecondary: '#252526',
    accentColor: '#3794ff',
  }}
  // ...
/>

All CSS variables are prefixed with --bt- and can be overridden via CSS as well.

Available Theme Variables

Variable Default Description
--bt-color-succeeded dark: #22c55e Succeeded result color
--bt-color-failed dark: #f14c4c Failed result color
--bt-color-running dark: #cca700 Running result color
--bt-color-idle dark: #8b8b8b Idle/no-result color
--bt-bg-primary dark: #1e1e1e Primary background
--bt-bg-secondary dark: #252526 Panel/sidebar background
--bt-bg-tertiary dark: #2d2d30 Inset/card background
--bt-text-primary dark: #cccccc Primary text color
--bt-text-secondary dark: #b3b3b3 Secondary text color
--bt-text-muted dark: #8b8b8b Muted/label text color
--bt-border-color dark: #3c3c3c Border color
--bt-accent-color dark: #3794ff Accent/selection color
--bt-grid-color derived Canvas grid color
--bt-minimap-mask derived Minimap viewport mask

Exports

// Component
export { BehaviourTreeDebugger } from '@bt-studio/react';

// Types
export type {
  BehaviourTreeDebuggerProps,
  PanelConfig,
  ThemeOverrides,
  ThemeMode,
  LayoutDirection,
  BTNodeData,
  BTEdgeData,
  TimeTravelState,
  TimeTravelControls,
  NodeDetailsData,
} from '@bt-studio/react';

// Utilities
export {
  DEFAULT_THEME,
  LIGHT_THEME,
  DARK_THEME,
  RESULT_COLORS,
  getResultColor,
  getFlagLabels,
} from '@bt-studio/react';