Anchor engine · January 2026 → April 2026Written by Claude Fable 5.1

The Anchor Tree

Anchor v1 answered "how do entities own other entities?" with one structure: a tree with a root called an, where every timer, collider, enemy and effect was somebody's child. This page explains what that tree did each frame, why it felt so good for a few thousand lines, where it stopped being the right tool, and what Anchor 2 uses instead.

The question that started it: "what are other ways to manage entities with parent–children relationships?" The tree is one answer. By the end there is a table of the others.

What the tree was

Everything in a v1 game was an object, and every object except the root had exactly one parent. self:add(child) did three things at once: it appended the child to self.children, it set child.parent, and, if either side had a name, it wired a field in both directions so a player could say self.timer and the timer could say self.player.

function object:add(child)
  table.insert(self.children, child)
  child.parent = self
  if child.name then
    if self[child.name] then self[child.name]:kill() end   -- same-named child: the old one dies
    self[child.name] = child
  end
  if self.name then child[self.name] = self end       -- and the child can reach its parent by name
  return self
end

That is the entire mechanism. Below is a small but honest game as v1 would hold it. Click any node: that runs the real kill semantics on it, marking the node and everything under it, and the sweep removes them the way the end of a frame would.

Click a node to kill() it. 0 marked dead
Ownership is the only edge. Names give two-way field access along an edge (player.timer, timer.player). Killing a node marks its whole subtree; nothing leaves the tree until the frame's cleanup runs, and cleanup destroys the deepest nodes first.

One frame

The tree was also the update order. The C engine called a single Lua update(dt) every physics tick, and that function walked the tree rather than the game. First it flattened an and all its descendants into one array with a depth-first search. Then it ran three passes over that same array: an early phase, a main phase, a late phase. Then it swept.

function update(dt)
  local all_objects = {an}
  for _, obj in ipairs(an:all()) do table.insert(all_objects, obj) end   -- DFS flatten, every tick

  for _, obj in ipairs(all_objects) do obj:_early_update(an:get_dt_for(obj)) end
  for _, obj in ipairs(all_objects) do obj:_update(an:get_dt_for(obj)) end
  for _, obj in ipairs(all_objects) do obj:_late_update(an:get_dt_for(obj)) end

  an:cleanup()   -- flatten AGAIN; drop finished actions; remove dead children, deepest first
end
1 · FLATTEN (DFS) 2 · THREE PASSES OVER THE SAME LIST 3 · CLEANUP, REVERSE ORDER (children destroyed before parents)
Nobody wrote this loop in game code. A player:update ran because the traversal reached it, between whichever siblings the DFS put around it. The three phases existed so a game could still express "input before movement before drawing" without controlling the order directly.

Two details of that loop matter later. an:all() allocated a fresh array and a fresh stack on every call, and the frame called it twice, at 144 ticks per second. And the phases were the only ordering tool: to make something happen before something else, you moved it to an earlier phase.

Why it worked

For the case it was designed around, composition, it was close to ideal, and it is worth being precise about why, because the same properties are what made it hard to leave.

Lifetime was free

A player owned a timer, a collider and a spring. When the player died, so did they, in the right order, with destroy called on each so the Box2D body was actually released. No game code tracked any of it.

enemy = object:extend()

function enemy:new(x, y)
  object.new(self)
  self.x, self.y = x, y
  self:add(collider('enemy', 'dynamic', 'circle', 16))   -- self.collider, dies with self
  self:add(timer())                                       -- self.timer, dies with self
end

function enemy:update(dt)
  if self.hp <= 0 then self:kill() end                       -- one call, whole subtree, resources included
end

Locality

The class was the whole description of the entity: its fields, its children, its behavior, its cleanup, in one place. That was the first principle of v1 and the tree is what made it cheap to honor.

Containers came for nothing

An object('projectiles') child was a group. self.projectiles:kill() emptied it. Named children replaced their predecessor automatically, so re-adding a 'shield' swapped the old one out.

Small things were tiny

Effects and decorations didn't need a class. An inline object with a one-shot action removed itself by returning true, and flow_to let you build the thing and then say where it lived.

cloud = object()
cloud:set({ x = px, y = py, speed = 12 })
cloud:action(function(self, dt)
  self.x = self.x + self.speed*dt
  bg:image(cloud_image, self.x, self.y)
end)
cloud:flow_to(self)   -- now it updates, draws and dies with its parent

For a game of a thousand lines, held by one person, this is a genuinely good model. The framework does the bookkeeping and the game reads like a description.

Where it broke

A tree encodes one relation: owns. A game needs about five, and the other four were the ones that grew with the codebase.

The tree draws only the solid edges. Every dashed one got its own bolt-on: tags and an:all('enemy') for aggregates, direct table references for back-links, physics queries for space, link for death events, flow_to for moving between states. Orblike ended up with all five patterns in one codebase and no rule for which to use when.

Ownership is a stronger claim than you meant

Because children never outlive parents, the tree forced a decision every time something was created: whose subtree does it live in? A bullet the player fires is the obvious example. Add it to the player and it vanishes the moment the player dies, mid-flight. So it goes into a projectiles container instead, and now its relationship to the player, the thing that actually matters for scoring and team, is a plain field with no lifetime story at all.

Kill cascades are not what every relationship wants. The tree made the wrong-by-default case (left) effortless and the right case (right) a manual reference the framework no longer protected.

The work was invisible

This is the deeper problem, and the one that scaled worst. Look at a v1 player:update(dt) and you cannot tell what happened that frame. Its timer ticked, but not in that function; the framework reached the timer child on its own traversal. Its collider synced positions in a phase you can't see from the source. An action registered somewhere else replaced self.move. A link from an enemy fired a callback on the player during the enemy's death. To debug, you held the traversal order in your head.

The frame's own edge cases show how much was being carried. From the code's documentation: adding the same child twice puts it in children twice and kills it twice; adding a child that already has a parent puts it in two arrays; all() returns dead objects and leaves the check to the caller; named actions live at self[name] and get nilled by cleanup. Each one is reasonable. Together they are a lot of rules for "a timer belongs to the player."

The amplifier

Every one of these problems got worse when an AI wrote most of the code. Claude made locally correct choices with globally wrong consequences: self:add for what should have been a field, link for what should have been polled, a different phase to paper over a timing bug. Each decision was defensible. The sum was a codebase whose implicit work nobody, human or model, could track at a glance.

What replaced it

The observation behind Anchor 2 is that most of the tree's machinery existed to solve problems the tree had created. The lifetime tracker, the subscription system, the phase ordering: each was a sophisticated answer to a question a flatter design never asks. So the rewrite replaced systems with disciplines.

No tree. Entities live in one map by id. Composition is a field you update yourself; a reference to another entity is its id, looked up when used. When an entity is destroyed it leaves the map, and every id pointing at it starts returning nil, which is the whole cleanup story.
v1 · the framework does it
function player:new(x, y)
  object.new(self)
  self:add(timer())
  self:add(spring())
  self:add(collider('player', 'dynamic', 'circle', 12))
  self:link(target, function(s) s.homing = false end)
end

function player:update(dt)
  -- timer, spring, collider updated elsewhere
  -- by the traversal, before or after this
  ...
end
v2 · the game does it
function player:new(x, y)
  make_entity(self)                -- self.id, registered
  self.timer = timer_new()
  self.spring = spring_new()
  self.collider = collider(self, 'player', 'dynamic', 'box', 12, 12)
  self.target_id = target.id       -- an id, not a pointer
end

function player:update(dt)
  timer_update(self.timer, dt)     -- every frame's work is here
  spring_update(self.spring, dt)
  local t = entities[self.target_id]
  if not t then self.homing = false end   -- poll, don't subscribe
  self.collider:sync()
end

function player:destroy()
  self.collider:destroy()          -- you list what you own
end

Each v1 feature has a v2 answer, and every answer is more verbose on purpose:

v1 mechanismWhat it solvedv2 discipline
Child update by traversalsub-objects tick without being mentionedPlain fields; timer_update(self.timer, dt) written where it runs
Kill cascadechildren never outlive parentskill() queues; process_destroy_queue() calls each entity's own destroy at frame end
Tags + an:all('enemy')aggregatesenemies = {} you maintain; collection_update compacts the dead
Direct referencesback-linksInteger ids resolved through entities[id]; a dead id is nil
linkreact when something diesPoll its state in your own update
flow_tore-parent, change stateConditional logic; string dispatch
Early / main / late phasesorderingWrite update(dt) in the order you want
The an rootone place for everythingPlain global tables and an explicit main loop the engine calls directly

The trade is exactly the one it looks like. v2 game code is longer because it does what the framework used to hide, and that is why it can be read top to bottom by someone who doesn't know the framework, including a model generating the next thousand lines of it. The rewrite wasn't a philosophy. It came from watching a real codebase pass the size where implicit work could still be tracked.

The other ways

So, the original question. "Parent–child" bundles at least four separate questions, and each design below answers them with a different mix. The v1 tree's real mistake was answering all four with one edge.

ApproachLifetimeUpdate orderCross-referencesShinesHurts
Ownership tree (Anchor v1)automatic cascadetraversal + phasesbolt-ons per needcomposition, containers, tiny inline objectseverything that isn't ownership; invisible work at scale
Scene graph (Godot, Unity transforms)cascadetraversal, with hooksnode paths, signals, groupshierarchies where the parent's transform must apply to children: UI, rigs, vehiclesgameplay relations that aren't spatial; nodes become the unit of everything
Flat map + ids (Anchor 2)explicit destroy, deferred queuethe code you wroteids resolved at use; stale is impossiblegameplay entities of a few dozen to a few thousand; readable framesverbosity; you maintain your own arrays
Handles with generationsslot reuse without stale accessexplicithandle = index + generationthe same as ids, with slot reuse for high churnmore machinery than a growing integer needs at Anchor's scale
ECSper-component removalsystems in a fixed orderentity ids; relations are componentsthousands of uniform things; cache-friendly iterationan entity's behavior is spread across systems; locality is gone by design
Parent pointer onlychild checks parentexplicitone upward linkattachments that follow a host (a weapon, a status)no downward iteration; containers need a second structure

The way to choose is to ask which question is doing the work. If children must inherit a parent's transform, you want a scene graph, and only for those objects. If entities need to know about each other across the whole game, you want ids in a flat map. If you have ten thousand of the same thing, you want an ECS. Anchor 2 keeps one class as an exception to its own rule, collider, because a body, a shape and a tag really are one thing; the principle is not "never a hierarchy," it's "a hierarchy only where the parent relationship is the truth of the object."

The v1 tree was a good answer to the first question that got asked to answer all of them. Anchor 2 is what it looks like to separate the questions again.

Sources · archive/Anchor/framework/anchor/object.lua and init.lua · Anchor Engine Overview (Jan 2026) · Anchor 2 Engine Overview (Apr 2026) · Anchor2/reference/anchor2_plan.md