Thunderous
Learning

A zero that looked like five hundred metres

Two bolts in tonight's session logged this: I was standing in a corridor about six metres long. The bolt did not travel five hundred metres. It travelled nothing at all — it was born inside the floor and died there on it…

Two bolts in tonight's session logged this:

[PlayerBolt] ended after 500.39 m on 'Wing_South_Walkway'
[PlayerBolt] ended after 500.46 m on 'Wing_South_Walkway'

I was standing in a corridor about six metres long. The bolt did not travel five hundred metres. It travelled nothing at all — it was born inside the floor and died there on its first frame.

The number is a zero wearing a costume, and I want to write down how it got the costume, because the mechanism generalises further than I expected.

Where the 500 comes from

Unity sets RaycastHit.point to Vector3.zero when the cast begins inside the collider it hits. This is documented, it is well known, and I have a paragraph about the adjacent version of it in my own project notes — Physics does not report a collider you start inside — which has cost me three separate bugs already.

So the sentinel is not the interesting part. This is:

Done(hit.collider.transform, hit.point);
...
var flew = Vector3.Distance(_bornAtPoint, at);

Four lines apart, the zero stops being a sentinel and becomes an operand. Vector3.Distance(muzzle, origin) is not a nonsense value. It is a perfectly well-formed measurement of a real distance — the distance from the muzzle to the world origin. My hub sits at x = 500 for reasons that have nothing to do with any of this, so the answer came out as 500.39 metres.

Everybody knows to check for a zeroed hit.point at the raycast. Nobody checks for it inside a Distance call in a different method, because by then it does not look like a sentinel any more. It looks like data.

Why it survived, which is the part worth keeping

PlayerBolt already had a counter for exactly this condition. A bolt that dies immediately is stillborn, and the test is:

else if (flew < StillbornDistance)   // 0.10 m

That is a good test. It is also unreachable in the case it exists for, because a stillborn bolt reports 500 metres. So boltsStillborn reads zero precisely when every bolt is stillborn — not uninformative, inverted. The counter I would reach for to diagnose this bug is the counter the bug disables.

I have a name for this shape now, because it is the third time this month: a check that cannot fire under the hypothesis it is testing. The first was a filter placed behind a probe too short to ever trigger it. The second was a test asserting health was unchanged, in a fixture where the player started at zero health, so it passed against the code it existed to refute. This is the third, and it is the sneakiest of them, because unlike the other two the number it produces is plausible.

And here is the generalisation I did not have before tonight: how convincing a sentinel-derived number looks is a function of how far your content sits from the world origin. If my hub were built around (0, 0, 0), that log line would have read ended after 0.42 m and I would have caught it the first time I read one. The 500 metres that made it survive is not a property of the bug. It is a property of my scene layout.

The other half of the evening, where the log and I disagreed

The same session produced a contradiction I could not resolve, and I think the honest handling of it is worth as much as the fix.

Thirteen bolt casts, every single one logging held by RTouch — the weapon is in the right hand. That is truthful: the code clears the holder only when the interaction SDK raises an Unselect, and it never did. The grab was live for all thirteen.

Meanwhile the blade's endpoints, which the same log line records, put the weapon about two metres horizontally from my head — outside arm's reach — sinking to 3.7 metres below the floor and then bobbing back up. And I was wearing the headset, and the sword was in my hand.

Both readings are honest. Holder is a proxy, and what it actually means is an interactor selected this and has not let go. It does not mean this object is following your hand. Those two are the same thing until they are not, and tonight they came apart.

I spent a while wanting to pick a side. The measurement is very specific and my memory of a moment in a headset is not; but a wearing is the only evidence that has ever settled anything in this project, and I have been wrong in exactly this way before.

What I actually did was neither. The cast line logs the blade and it logs my head and it does not log my hand — so "the weapon left your hand" and "the weapon's transform diverged from the mesh you can see" produce byte-identical output. There is no adjudicating between them with the instrument I have. So the next build adds one value to one log line, and the next cast answers it.

That is the whole lesson and it is not a new one, I just keep having to relearn the cost: when a value stands in for a body, log the body next to it. A proxy that agrees with reality most of the time is not a bug you find by thinking harder about the proxy.

Nothing got fixed tonight, deliberately — my own end-of-session rule is that a refactor made after the last wearing is worse than ending on a green suite. 1394 of 1396 tests pass, two long-standing skips, and the sword looks fantastic.