Good narrowing. Your instinct in the earlier post was right: the angle is a symptom, not the cause. I went back into the engine source and found the actual break. It is in the save round trip, not in your events.
The save loses null
“Save to a variable” writes the JS state into a GDevelop variable with variable.fromJSObject(gameSaveState) (Extensions/SaveState/SaveStateTools.ts). And fromJSObject does this (GDJS/Runtime/variable.ts):
if (obj === null) {
this.setString('null'); // the string, not null
}
So every JS null in the save data comes back as the string "null", and toJSObject() hands that string to the loader. Every === null check then misses, and any arithmetic on it gives NaN.
Why it hits 3D models
A 3D model is one of the few objects that stores real nulls. Its origin point and center point are [float|null, ...] (LocationPoint in Extensions/3D/Model3DRuntimeObject.ts), and the defaults are “Model origin” = [null, null, null] and “Centered on Z” = [null, null, 0.5] (Model3DObjectConfiguration.cpp). Those are saved as op and cp.
On load, this._originPoint = networkSyncData.op || [null, null, null] keeps the strings, because the array is still truthy. Then in the renderer (Model3DRuntimeObject3DRenderer.ts):
point[0] = originPoint[0] === null ? this._modelOriginPoint[0] : originPoint[0];
originPoint[0] is "null", so the fallback is skipped and the string is returned. Then:
position.set(getX() - width * (originPoint[0] - centerPoint[0]), ...);
"null" - "null" is NaN. The model is in the scene, its instance x/y/angle read fine, and its transform is NaN, so it is never drawn. stretchModelIntoUnitaryCube also takes the wrong branch and writes NaN into the position.
Same story for NavMesh Pathfinding: d: this._path[this._path.length - 1] || null (NavMeshCharacterRuntimeBehavior.ts), and the loader tests d === null, which is false after the round trip, so it calls moveTo("null".x, ...).
Confirm it in two minutes
- Save, load, then expand the 3D model’s entry in the save variable in the debugger and look at
op and cp. If they read as null text, that is the string.
- Fastest test: set Origin point and Center point to “Object center” (stores 0.5s, no nulls), save, load, then move it. If it stays visible, this is the bug.
- For the angle: after load, log
ToString(Model3D.Angle()). "null" means the angle got poisoned the same way; NaN means it was computed from a bad value. The debugger panel cannot tell you which, because its messages go through JSON, and JSON turns NaN into null.
Until it is fixed
Keep origin/center off “Model origin” and “Centered on Z” on anything you save, and re-apply position and angle after load with ToNumber(...).
Worth a GitHub issue with the variable dump attached. It is a clean repro: any scene with a 3D model, save to a variable, load, move it.