Astra the Luminid close to the camera in a green meadow
She had a face the whole time. We were measuring from the wrong thing.

Three bugs came out of August that were more useful than most of the features we shipped, and they were all the same bug wearing different clothes.

In each case the instrument worked perfectly. The code ran, returned a value, and reported success. The problem was that the question it was asking was subtly wrong, so it kept answering confidently and wrongly for months.

If you work in Godot, the first one is the one to take away.

1. A recursive node search in the thinking loop

Every Luminid runs a thinking loop: it looks at its needs, its job, what is near it, and decides what to be doing. With a colony of a couple of hundred creatures, that loop runs a lot.

Somewhere inside it was a single call that searched the entire node tree, recursively, for a node by name.

Godot makes this extremely easy to write. find_child() defaults to recursive, it reads beautifully, and on a small scene it costs nothing measurable. On a live colony scene it walks a very large tree, and it was doing that once per creature per think.

Worst-case think time before 1,331 ms
Worst-case think time after 2.6 ms
Improvement roughly 500×

One line. A second of frame time in the worst case, on a game whose entire promise is that it will not stress you.

The fix is the boring one: resolve the node once, cache the reference, and never search the tree from inside anything that runs per-frame or per-agent. Godot gives you @onready, unique names with %, and plain exported NodePath properties precisely so you do not have to search at runtime.

The uncomfortable part: there are still around fourteen hundred tree-searching calls elsewhere in this codebase. Almost all of them are fine, because they run once at setup. The dangerous ones are the handful that ended up inside a hot loop, and nothing in the language or the editor distinguishes between the two. That is a category of bug you have to go looking for.

If you take one thing from this post: search your project for recursive node lookups and check which of them are inside something that runs every frame. It is a ten minute audit and it is the highest-yield ten minutes we spent all month.

2. We were not running the physics engine we thought we were

Luminids is meant to run on Jolt. We had made that decision deliberately, written it down, and built around it.

We had a check that confirmed it at startup. The check compared the string "JoltPhysics" against the string "Jolt Physics".

Those are not the same string. The comparison failed, the code took its fallback path, and the fallback path was silent. So for a long time we were running on a physics backend we had not chosen, while a green check told us we were fine.

A Luminid in open grass with a small animal nearby
Once we were actually running the engine we had chosen.

The lesson is not “be careful with strings”. It is that a check which can fail silently is worse than no check, because it converts an open question into a settled one. If a configuration gate matters, it should be loud when it does not match: log it, assert it, or refuse to boot.

3. The creatures had no faces, from the only angle that mattered

This one is my favourite because it is so obviously correct until it isn’t.

Solving a creature’s face is not free, so we only do it when someone is close enough to see it. Perfectly reasonable optimisation. “Close enough” was measured as distance from the player’s body.

But the player’s body is not what looks at things. The camera is. And in this game the camera is very often somewhere else entirely: the overhead build camera, photo mode, a cutscene rig, or a third-person boom sitting metres behind the character.

So a Luminid forty metres from my body and two metres from my lens was outside the “close” threshold. Its face stayed frozen at whatever it last resolved to, which for a creature approached from behind is nothing at all. I could orbit a creature at arm’s length and it had no face from any angle.

The general form: an optimisation gated on proximity should almost always be gated on the camera, not the player. Any time those two can separate — and in most 3D games they can — the player-based version will be wrong exactly when someone is looking closely.

The bonus one: the telemetry was lying too

We ran a playtest in the middle of the month and then spent two days repairing the instruments that measured it, because almost every number in the first export was wrong.

Two thirds of the rows were my own machine. The export was sorted backwards, which made the median load time look about thirty-five times faster than it actually was.

Fixing the telemetry was, genuinely, the most valuable engineering of the month. Every decision downstream of a measurement inherits that measurement’s error, and a confidently wrong dashboard will send you off optimising something that was never slow.

The pattern

Four failures, one shape:

  • The tree search worked — it returned the right node, catastrophically slowly.
  • The physics check worked — it compared two strings correctly and drew the wrong conclusion.
  • The face gate worked — it measured a real distance to the wrong object.
  • The telemetry worked — it exported real rows, sorted so the summary was meaningless.

Not one of them threw an error. Not one of them showed up as a failing test. Every one of them reported success while being wrong.

We now assume every gate is blind until proven otherwise, and the way you prove otherwise is not by reading the code. It is by watching the thing run and asking whether what you see matches what the number says. In Dev Log 17 the same lesson arrived from the other direction: the tests said a migration was complete and ninety minutes of watching said it was not.

Green tests are evidence. They are not a verdict.

Nick


Luminids is a cozy colony-building sandbox built in Godot 4, coming to PC via Steam. Read what it actually is, or how we built the island.