diff --git a/examples/TicTacToe/test_load.txt b/examples/TicTacToe/test_load.txt
new file mode 100644
index 0000000..1f5a4b4
--- /dev/null
+++ b/examples/TicTacToe/test_load.txt
@@ -0,0 +1,3 @@
+y
+1 0
+0 2
\ No newline at end of file
diff --git a/examples/TicTacToe/test_save.txt b/examples/TicTacToe/test_save.txt
new file mode 100644
index 0000000..498f27a
--- /dev/null
+++ b/examples/TicTacToe/test_save.txt
@@ -0,0 +1,3 @@
+0 0
+1 1
+0 1
\ No newline at end of file
diff --git a/src/OECS/WorldSerializer.cs b/src/OECS/WorldSerializer.cs
new file mode 100644
index 0000000..0a8ecec
--- /dev/null
+++ b/src/OECS/WorldSerializer.cs
@@ -0,0 +1,91 @@
+using MessagePack;
+
+namespace OECS;
+
+///
+/// Saves and loads a to/from a MessagePack stream.
+///
+/// Components are serialized by their runtime type. The caller must
+/// ensure all component types used in the world are resolvable via
+/// at load time.
+///
+public static class WorldSerializer
+{
+ ///
+ /// Saves the world state to a stream.
+ ///
+ public static void Save(World world, Stream stream)
+ {
+ var entityComponents = new Dictionary>();
+
+ foreach (var ct in world.Components.ComponentTypes)
+ {
+ var set = world.Components.GetSet(ct);
+ if (set == null || set.Count == 0) continue;
+
+ var entities = set.GetDenseEntities();
+ for (int i = 0; i < set.Count; i++)
+ {
+ var entity = entities[i];
+ var component = set.GetComponentAt(i);
+
+ if (!entityComponents.TryGetValue(entity, out var list))
+ {
+ list = new List();
+ entityComponents[entity] = list;
+ }
+
+ list.Add(new ComponentEntry
+ {
+ TypeName = ct.AssemblyQualifiedName!,
+ Data = MessagePackSerializer.Serialize(ct, component)
+ });
+ }
+ }
+
+ var snapshot = new WorldSnapshot
+ {
+ Entities = entityComponents
+ .Select(kv => new EntitySnapshot
+ {
+ Id = kv.Key.Id,
+ Version = kv.Key.Version,
+ Components = kv.Value.ToArray()
+ })
+ .ToArray()
+ };
+
+ MessagePackSerializer.Serialize(stream, snapshot);
+ }
+
+ ///
+ /// Loads the world state from a stream, adding entities and components
+ /// to the given world. The world should be empty or the caller is
+ /// responsible for managing duplicate entities.
+ ///
+ public static void Load(World world, Stream stream)
+ {
+ var snapshot = MessagePackSerializer.Deserialize(stream);
+
+ foreach (var es in snapshot.Entities)
+ {
+ var entity = world.CreateEntity(es.Entity);
+
+ foreach (var ce in es.Components)
+ {
+ var type = Type.GetType(ce.TypeName)
+ ?? throw new InvalidOperationException(
+ $"Unknown component type '{ce.TypeName}'. " +
+ $"Ensure the assembly is loaded.");
+
+ var component = MessagePackSerializer.Deserialize(type, ce.Data)
+ ?? throw new InvalidOperationException(
+ $"Failed to deserialize component of type '{ce.TypeName}'.");
+
+ var method = typeof(World).GetMethod(nameof(World.AddComponent))!
+ .MakeGenericMethod(type);
+ method.Invoke(world, [entity, component]);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/OECS/WorldSnapshot.cs b/src/OECS/WorldSnapshot.cs
new file mode 100644
index 0000000..fd80802
--- /dev/null
+++ b/src/OECS/WorldSnapshot.cs
@@ -0,0 +1,39 @@
+using MessagePack;
+
+namespace OECS;
+
+///
+/// Serializable snapshot of the entire world state.
+///
+[MessagePackObject]
+public class WorldSnapshot
+{
+ [Key(0)] public EntitySnapshot[] Entities { get; set; } = [];
+}
+
+///
+/// A single entity and all its components, serialized as name/data pairs.
+/// Entity ID and Version are stored separately to avoid issues with
+/// MessagePack deserialization of the opaque Entity struct.
+///
+[MessagePackObject]
+public class EntitySnapshot
+{
+ [Key(0)] public uint Id { get; set; }
+ [Key(1)] public uint Version { get; set; }
+ [Key(2)] public ComponentEntry[] Components { get; set; } = [];
+
+ [IgnoreMember]
+ public Entity Entity => new(Id, Version);
+}
+
+///
+/// A single component, identified by its fully-qualified type name and
+/// serialized as a MessagePack byte blob.
+///
+[MessagePackObject]
+public class ComponentEntry
+{
+ [Key(0)] public string TypeName { get; set; } = "";
+ [Key(1)] public byte[] Data { get; set; } = [];
+}
\ No newline at end of file