Table of Contents

Serialization Pattern

This page documents the manual world serialization pattern demonstrated in src/Test/Ghost.Entities.Test/SerializationTest.cs.

Scope

The current test shows a low-level JSON snapshot workflow for ECS data:

  • Enumerate archetypes and chunks.
  • Read entity IDs and raw component bytes.
  • Convert bytes to managed instances.
  • Serialize component payloads as JSON.
  • Parse JSON back into typed objects.

This is useful for debugging, tooling, and prototype save/load workflows.

Write Path (World -> JSON)

Core flow from the test:

using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true });

writer.WriteStartObject();
writer.WriteString("Name", "world 1");
writer.WriteStartArray("Entities");

for (var i = 0; i < world.ComponentManager.ArchetypeCount; i++)
{
    ref var archetype = ref world.ComponentManager.GetArchetypeReference(i);

    for (var j = 0; j < archetype.ChunkCount; j++)
    {
        ref var chunk = ref archetype.GetChunkReference(j);
        for (var k = 0; k < chunk._count; k++)
        {
            writer.WriteStartObject();
            writer.WriteNumber("ID", entityID);
            writer.WriteStartArray("Components");

            // For each component in archetype layout:
            // - resolve runtime type
            // - marshal bytes to managed object
            // - JsonSerializer.Serialize(writer, instance, type, options)

            writer.WriteEndArray();
            writer.WriteEndObject();
        }
    }
}

writer.WriteEndArray();
writer.WriteEndObject();
writer.Flush();

Data shape used in the test:

{
  "Name": "world 1",
  "Entities": [
    {
      "ID": 1,
      "Components": [
        {
          "Type": "Full.AssemblyQualified.TypeName",
          "Data": { }
        }
      ]
    }
  ]
}

Read Path (JSON -> Typed Data)

The test parses JSON and reconstructs typed component instances:

var root = JsonDocument.ParseValue(ref reader).RootElement;
var entityData = new List<(int EntityID, Type ComponentType, object Instance)>();

foreach (var entityElement in root.GetProperty("Entities").EnumerateArray())
{
    var id = entityElement.GetProperty("ID").GetInt32();

    foreach (var componentElement in entityElement.GetProperty("Components").EnumerateArray())
    {
        var typeName = componentElement.GetProperty("Type").GetString();
        var type = Type.GetType(typeName!);
        if (type == null)
        {
            continue;
        }

        var instance = componentElement.GetProperty("Data").Deserialize(type, options);
        if (instance != null)
        {
            entityData.Add((id, type, instance));
        }
    }
}

Caveats

  • The example is intentionally low-level and may use internal layout details.
  • AssemblyQualifiedName based type resolution is convenient but can be brittle across assembly/version changes.
  • For production save/load, prefer stable type identifiers and versioned schemas.
  • Reflection + marshaling has overhead; avoid in per-frame hot paths.
  • Inspecting world state during development.
  • Exporting compact debugging snapshots.
  • Prototyping serializer architecture before introducing a dedicated format.

For public runtime API details, see generated docs in doc/api/.