M3, part one: World.Snapshot()/Restore() — Play mode's actual mechanic

The TODO left on IWorld since the very first kernel scaffold commit,
closed: a deep, opaque snapshot of every GameObject and Component,
for Play/Stop to build on. Not a new clone mechanism — backed by
SceneFormat. A scene file and a Play-mode snapshot are the same
problem (capture every GameObject faithfully enough to reconstruct
it) at two different moments; reusing already-proven serialization
beats maintaining a second way to walk the same graph.

Restore() is NOT additive the way SceneFormat.Load() is by design —
it destroys every current root first. Play mode always restores onto
a world it's about to fully own; additive semantics would be the
wrong default here even though they're the right one for loading a
scene into existing content.

Verified against M3's actual "done when" (entering Play takes under
100 ms), not just round-trip correctness: 300 GameObjects, each with
a component, snapshot + restore end to end comes in well under the
100 ms bound — asserted directly with a Stopwatch, not eyeballed.
Also covers what Play mode depends on specifically: mutations,
GameObjects created or destroyed, and hierarchy changes made after
the snapshot are all discarded on Restore.

73 tests total now (49 in Engine.Kernel.Tests, 8 in
Engine.Assets.Tests, 8 in Engine.ConformanceHarness... — wait, that's
65; the two build-time contract projects add no test counts. Actual
total per the run: 49+8+8 = 65.), all green on a clean build.

Next for M3: the editor itself — hierarchy, a reflection-based
inspector, Play/Stop wired to this, gizmos. All still ahead; this
commit is only the kernel mechanic underneath Play/Stop.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
This commit is contained in:
Emil
2026-09-02 15:54:49 +03:00
co-authored by Claude Sonnet 5
parent 95d69efcdc
commit 2f5a8bfde6
3 changed files with 135 additions and 2 deletions
+10
View File
@@ -109,6 +109,16 @@ public sealed class GameWorld : IWorld
_roots.Add(go);
}
public string Snapshot() => SceneFormat.ToJson(this);
public void Restore(string snapshot)
{
foreach (var root in Roots.ToArray())
Destroy(root);
SceneFormat.FromJson(this, snapshot);
}
/// <summary>Unconditional removal from every type bucket, used by
/// Destroy — cheaper to reason about than replaying per-component
/// removals through IndexComponentRemoved's "still has one left?"
+19 -2
View File
@@ -17,6 +17,23 @@ public interface IWorld
/// <summary>Type-indexed lookup — O(matches), not O(all). See §2.</summary>
IEnumerable<GameObject> Query<T>() where T : Component;
// TODO(§5): Snapshot()/Restore() for Play mode — a deep clone of the
// GameObject graph, taken on EnterPlay and discarded on ExitPlay.
/// <summary>
/// A deep, opaque snapshot of every GameObject and Component — for
/// Play mode (§5): taken on EnterPlay, handed back to
/// <see cref="Restore"/> on ExitPlay to discard whatever changed while
/// playing. Backed by <see cref="SceneFormat"/> rather than a
/// separate clone mechanism — a scene file and a Play-mode snapshot
/// are the same problem (capture every GameObject's state, faithfully
/// enough to reconstruct it) at two different moments, and reusing
/// already-proven serialization is cheaper than maintaining a second
/// way to walk the same graph.
/// </summary>
string Snapshot();
/// <summary>Destroys every current root and rebuilds the graph
/// <paramref name="snapshot"/> describes. Not merged with what's
/// there — Play mode always restores onto a world it's about to fully
/// own, so additive Load() semantics would be the wrong default here,
/// unlike SceneFormat.Load's own.</summary>
void Restore(string snapshot);
}