"Resolve identity by registry, not by walking the tag chain"
▸ SYMPTOM
- A feature that resolves "which object owns this collision" works, then quietly stops firing after an unrelated change — no exception, no log line, just a feature that never triggers.
- A roster/registry lookup starts answering
null(or the wrong object) for contacts that used to resolve correctly. - The break correlates with a different system starting to apply an existing tag to child objects.
▸ CAUSE
A resolver that walks a collided GameObject's parent chain for the first ancestor carrying a tag is only correct while exactly one object per branch carries that tag:
// Fragile: "first X-tagged ancestor wins"
GameObject ResolveOwner( GameObject hit )
{
for ( var go = hit; go is not null; go = go.Parent )
if ( go.Tags.Has( "actor" ) )
return go; // stops on the NEAREST match, not the root
return null;
}The walk terminates on the nearest match. The instant any later pass tags a descendant with the same tag, the walk stops on that descendant instead of the intended root, and every downstream caller gets the wrong object — or null.
The classic trigger is a tag doing double duty: physics filtering and identity lookup. One system tags detachable parts' own collision shapes with the actor's identity tag (so a detached part can still be told apart), while another system walks that same tag to find the owning root. Nothing enforces that only one object per branch carries the tag, so the two systems silently fight. In the live case, every part-to-part contact resolved to the part instead of the root, the roster lookup answered null, and hit registration was silently dropped — nothing observably wrong except a feature that quietly never fired.
▸ FIX
Resolve identity against an authoritative roster/registry — ask each candidate "are you an object I own?", never "are you tagged X?":
// Robust: ask the registry, don't walk tags
GameObject ResolveOwner( GameObject hit )
{
for ( var go = hit; go is not null; go = go.Parent )
if ( roster.Owns( go ) ) // authoritative membership test
return go;
return null;
}The registry is the single source of truth for "who is a real actor," so a tag applied to a child for an unrelated reason can never redirect the walk.
Use a tag-walk resolver only when the tag has a single, dedicated purpose and no other system can stamp it onto a descendant. The moment two independent systems can both decide to apply the same tag — or the tag is also used for physics filtering — a tag-walk resolver is a liability.
▸ WHY IT WORKS
Tags in s&box are a flat, shared namespace: any system can add any string tag to any GameObject, and there's no built-in "only one per branch" invariant. Identity is a domain concept your game owns, so the authoritative answer lives in your own roster/registry, not in a string that multiple subsystems are free to reuse. Testing membership against that registry decouples identity resolution from whatever else the tag is being used for.
- Published