m05 · Baking · reading · 10 min
Prefabs and blob assets — what gets shared instead of copied
The copy problem
Everything so far bakes authored data into entities: a value on a
MonoBehaviour becomes a value in a component, one copy per entity. For
small per-entity values that’s exactly right — that’s what
But two kinds of data break it. Data that many entities must share identically (a 4 KB pathfinding grid every agent reads), and data that describes a thing to be created later (the enemy prefab a spawner instantiates). Copying either into every entity is wrong, and each is wrong for its own reason. Baking handles them with two different mechanisms, and the mechanisms are worth deriving rather than memorizing.
Prefabs: the entity that exists but doesn’t run
Start with the spawner. A spawner needs to create enemies at runtime. Module 1 says instantiation copies an existing entity’s archetype and component values into a new entity — so instantiating requires a source entity that already has the right shape and data.
Where does that source come from? It can’t be constructed at runtime; that would mean writing in code the archetype and values a designer authored, duplicating the authoring work in C# and guaranteeing drift. It should be baked, like everything else authored.
But a baked source entity would then exist in the world — and every query matching its archetype would pick it up. Your enemy prefab would be rendered, moved by the movement system, and damaged by the damage system, sitting at the origin as a ghost enemy nobody placed.
Both requirements have to hold at once: be a fully baked entity with the
correct archetype and values, and be invisible to ordinary queries. That is
precisely what a Prefab tag component does. It’s a zero-size tag (Module 1),
and the query system excludes Prefab-tagged entities by default. The entity
is real, instantiable, and unmatched.
Notice you just derived a piece of API from constraints, which is the test §0 sets. Given “instantiation needs a source entity” plus “sources must not participate in gameplay,” a default-excluded tag is the obvious answer — and it costs zero bytes because tags carry no data.
In a baker, this is GetEntity(prefabGameObject, TransformUsageFlags.Dynamic)
stored in a component, so the spawner holds an Entity handle to its source —
one of the references the last lesson said gets remapped on load. The pieces
interlock.
Blobs: why big shared data can’t be a component
Now the pathfinding grid. Suppose 10,000 agents each need to read the same 4 KB navigation data.
Putting it in a component is catastrophic, and Module 1 gives the exact price.
A chunk is 16 KB. A component of 4 KB means three entities per chunk —
before any other component. Your iteration loses every property that made it
fast: no density, no prefetch benefit,
So it must be stored once, outside the chunk, with entities referencing it. Which raises the question that determines the mechanism: why not just a pointer?
Three reasons, each fatal on its own. A managed C# reference can’t live in an
IComponentData — components are unmanaged, and Burst-compiled jobs can’t
touch managed objects at all (Module 3). A raw pointer can’t be serialized —
the address is meaningless in the next process, so a baked scene containing
pointers is garbage on load. And a pointer gives the safety system nothing to
reason about; jobs sharing it get no read/write analysis (Module 4).
A blob asset is the construct that satisfies all three. It is immutable,
unmanaged, position-independent data with a BlobAssetReference<T> handle that
is Burst-compatible, job-safe, and — critically — serializable, because
internal links inside a blob are stored as relative offsets rather than
absolute addresses. A blob doesn’t care where in memory it lands; every
reference inside it is “so many bytes from here.” That single design choice is
what lets baked data contain structured, linked information at all.
Immutability isn’t a limitation bolted on; it’s what makes the sharing safe.
If a blob could be written, 10,000 entities referencing it across parallel jobs
would be a data race by construction, and the safety system’s whole argument
(Module 4) collapses. Read-only shared data has no write to overlap, so every
reader parallelizes freely — the [ReadOnly] argument, applied to an asset
instead of a container.
The chunk cost of all this: BlobAssetReference<T> is a handle, so the
component holding it is a few bytes. Density preserved, data shared once,
Burst-safe, serializable.
The decision, stated as a rule
You now have the criteria, so the rule is derivable rather than memorized:
Copy it into the component when the value is small and genuinely per-entity — position, health, speed. Chunk density is the point, and small unique values are what chunks are for.
Put it in a blob when the data is large, immutable, and shared. The threshold isn’t a magic byte count; it’s the moment per-entity copies start costing chunk capacity that no system’s access pattern justifies. Mesh collision data, navigation grids, authored curves, lookup tables.
Make it a prefab when the authored thing is a template for entities not yet created, rather than data an existing entity holds.
What this buys you
You can now explain why a prefab must be a real entity hidden from queries by a tag, why large shared data must leave the chunk, and why the thing it leaves into has to be immutable, unmanaged and offset-addressed rather than a pointer. That completes the baking picture: values copied, templates tagged, shared data blobbed, references remapped. Next, the lab — you will measure the chunk-density cost of a baking decision directly.