Skip to content

Editor tools ​

GodotECS ships editor tooling under GodotECS.Editor plus the plugin entry point. Everything is built in code (no .tscn dependencies), and the expensive logic is kept UI-free so it can be unit-tested without the scene tree.

Plugin setup ​

The addon entry point is plugin (addons/godotecs/plugin.cs, a [Tool] EditorPlugin compiled only with TOOLS):

  • _EnterTree() creates an EntityDebuggerDock named "ECS Debugger" and adds it to DockSlot.LeftUr, then wires the companion tools: a BakeTool (the Bake section inside the dock), an EntityInspector (non-visual) as dock children, and an EcsGizmos node parented to a 3D host — the editor 3D viewport first, else the edited scene root (a Node3D under a Control never renders in the 3D view).
  • _ExitTree() removes and frees everything.

Enable it via Project → Project Settings → Plugins → GodotECS → Enable. plugin.cfg declares name GodotECS, version 0.2.0, script plugin.cs.

Debugger dock ​

EntityDebuggerDock (Control) is the main ECS panel. UI, built in _Ready():

  • World selector (OptionButton: Default / Preview) — switches CurrentWorld.
  • Info label — World: <name> Tick: <tick> Live: <live> Query(All): <count>.
  • Per-system label — the top-20 systems by average ms (EMA smoothing: 0.9·prev + 0.1·sample; fed by any system or bridge code calling RegisterSystemTiming(name, ms)).
  • Hash label — hash: <X16> bytes: <n> from a world snapshot (Snapshot.Save + Snapshot.HashBytes), or hash: err <Exception> on failure. Comparing hashes across runs is the quick determinism check.
  • Timing label — avg: <ms> ms over <n> campioni, with a [PAUSA] marker when paused.
  • TimingGraph — an embedded bar chart of the last up-to-120 frame times with an average line.
  • Buttons: Pausa/Riprendi, Step, Refresh.

Workflow logic (all callable without the tree):

  • Pause/step: SetPaused(bool) / TogglePause() freeze the timing feed; RequestStep() sets a one-shot flag consumed via ConsumeStep() — advance exactly one tick while paused.
  • Timings: RegisterTiming(float ms) pushes into a 120-sample ring buffer (TimingCapacity), sanitizing negative/NaN/infinity to 0. AverageMs(), GetTimingsOrdered() (oldest-first copy), TimingCount, ClearTimings(). TopSystems(20) returns the per-system EMA table sorted by ms, ClearSystemTimings() resets it.
  • Hashing is throttled: the periodic refresh (every 0.5 s) skips Snapshot.Save; a dedicated timer recomputes the hash every HashInterval (2 s) via RefreshHash() (SnapshotSaveCount counts the saves). The Refresh button / RefreshNow() includes the hash on demand.
  • Worlds: GetWorlds() returns { World.Default, World.Preview }; SelectWorld(index) switches (out-of-range ignored) and refreshes.
  • ComputeHash(World) is Snapshot.Hash(w).
  • _Process records delta * 1000 as a timing sample when unpaused and refreshes the labels every 0.5 s (RefreshInterval).

Typical session: open the dock, watch live count, average ms and the slowest systems while playing, pause + step to inspect a spike, compare snapshot hashes between two runs to confirm determinism.

Inspector ​

EntityInspector (Node, non-visual) answers "what is this rendered thing?" and "what is on this entity?":

  • MultiMesh → entity mapping: the render sync registers each drawn instance via RegisterMapping(multimeshInstanceId, instanceIndex, entity). Clicking a rendered instance raycasts to its MultiMesh + index, and TryGetEntity(...) resolves the Entity. UnregisterMapping(...) removes one entry, ClearMappings() all, MappingCount reports the size.
  • Inspect(World, Entity) returns a Dictionary with world, tick, index, version, alive, liveCount (null world → World.Default).
  • InspectWith<T>(World, Entity) adds has_<TypeName> plus data_<TypeName> (ToString() of the component, or err:<Exception> on failure) for any struct : IComponentData.

Design note: this is the inspection backend, not the Godot property inspector — the dock or a custom inspector panel calls it and renders the dictionary.

Gizmos ​

EcsGizmos (Node3D) draws world-space AABBs (GodotECS.Math.Aabb) as green wireframe lines through an ImmediateMesh (unshaded, no shadows):

  • AddBox(in Aabb) appends one box; SetBoxes(IEnumerable<Aabb>) replaces the set (null clears); ClearBoxes() empties it. Every mutation marks the mesh dirty.
  • Rebuild() redraws all 12 edges per box immediately; _Process rebuilds only when dirty, and hides the mesh node entirely when ShowGizmos is false or the node is not visible in the tree.
  • BoxCount reports the pending box count; ShowGizmos (default true) is the master toggle.

Intended contents: chunk AABBs, broadphase grid cells, or velocity vectors for the current selection — never one gizmo per entity at 100k scale.

Bake tool ​

BakeTool (VBoxContainer) is a Bake ECS button plus a status label for editor use, wired directly to GodotECS.Authoring.Baker (no reflection since v0.2):

  • TryBake(Node root) clears World.Preview and bakes the subtree into it via BakeSubtreeInto, reporting baked-subtree:<n> and setting LastBakedCount.
  • TryBake() without a node returns false with status no-root — baking requires an explicit subtree.
  • LastStatus records the outcome (idle, no-root, baked-subtree:<n>, bake-error) and mirrors it to the label. Failures PushWarning and return false without modifying any scene.
  • BakeTool.ClearPreview() empties World.Preview (DestroyBatch), so consecutive bakes never accumulate leftover entities.

Full API ​

Exact signatures for plugin, EntityDebuggerDock (+TimingGraph), EntityInspector, EcsGizmos, and BakeTool: Editor API.

GodotECS docs — version 0.2.0. All rights reserved until a license is added.