m05 · Baking · reading · 10 min
The Baker — why conversion is declared, not called
The requirement that shapes everything
The last lesson ended on a cost: baked output is cached, so the system must know what to re-bake when a designer changes a value. Take that requirement seriously for a moment, because it — not convenience — determines the entire shape of the baking API.
A scene has 50,000 authored objects. A designer nudges one float on one of them. What should re-bake?
If the answer is “everything,” iteration is dead — a one-character change costs a full-scene conversion, and the designer stops iterating. If the answer is “only the object that changed,” you’re wrong whenever one object’s baked output depended on another’s data, and you ship stale bytes. The correct answer is: re-bake exactly those conversions whose inputs changed — which requires knowing, per conversion, precisely which authored values it read.
That knowledge cannot be inferred from arbitrary C#. If conversion were a function you wrote freely, it could read anything — a sibling component, a referenced prefab, a static field — and no tooling could determine what it touched. Incremental re-baking would be undecidable, so the system would have to conservatively re-bake everything, which is the dead case.
So the API must be shaped to make reads observable. That constraint gives you the Baker.
A Baker is a declaration with a tracked input surface
A Baker is a class parameterized on one authoring component type. You don’t call it; you declare it, and the baking pipeline finds it and runs it for every instance of that component in the scene.
public class EnemyAuthoring : MonoBehaviour
{
public float Speed = 5f;
public int Health = 100;
}
public class EnemyBaker : Baker<EnemyAuthoring>
{
public override void Bake(EnemyAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new Speed { Value = authoring.Speed });
AddComponent(entity, new Health { Value = authoring.Health });
}
}
Two things about that shape are doing real work.
The type parameter is the trigger. Baker<EnemyAuthoring> tells the
pipeline “run me for each EnemyAuthoring.” Registration is by type, not by a
list you maintain, which means adding a new authoring component adds a new
baker file and nothing else — the same content-is-data discipline this course
applies to its own lessons.
The methods are the tracked surface. GetEntity, AddComponent,
GetComponent, DependsOn — every one is a method on the Baker, not a free
call. That is not stylistic. Each call is a point where the pipeline observes
what you touched, and records it as a dependency of this conversion. The Baker
isn’t a helper object; it is an instrumentation boundary.
Reading through the Baker versus reading around it
Here is the distinction that separates working bakers from ones that ship stale data, and it is the most important thing in this lesson.
// TRACKED — the pipeline sees this read and records the dependency.
var rb = GetComponent<Rigidbody>();
// UNTRACKED — an ordinary Unity call. The pipeline never learns about it.
var rb = authoring.GetComponent<Rigidbody>();
Both lines retrieve the same object. Only the first registers that this
conversion’s output depends on that Rigidbody. Read through the Baker and
editing the Rigidbody re-bakes this entity. Read around it and editing the
Rigidbody changes nothing, because the pipeline has no reason to believe
anything downstream cared.
The same rule extends to anything a conversion consumes that isn’t its own
authoring component’s fields: referenced assets, prefabs, sibling components,
transforms. DependsOn(someAsset) exists precisely for the case where you use
a value the pipeline couldn’t otherwise observe you using.
Why GetEntity takes transform usage flags
One detail in that first example deserves its own explanation, because it’s where readers usually accept an incantation instead of deriving it.
var entity = GetEntity(TransformUsageFlags.Dynamic);
Every authored object has a Transform — position, rotation, scale. Naively,
every baked entity should therefore get transform components. But Module 1
priced that: components are chunk-space, and LocalTransform, and adding one to a million static props costs real capacity
per chunk for data no system reads.
The flags let each baker declare how this entity’s transform will actually
be used, and the pipeline adds only the components that usage requires:
Dynamic for things that move, Renderable for things drawn but static,
None for pure data entities with no spatial existence. It is the same
describe-don’t-command inversion you’ve now seen at every layer — you state
the requirement, the pipeline derives the archetype.
Which produces the module’s recurring theme in miniature: baking decisions are archetype decisions. What a baker adds determines chunk shape, and chunk shape determines the throughput ceiling of every system that touches those entities. A careless baker is a performance bug that manifests three modules away, in a system that looks innocent.
What this buys you
You can now explain why conversion had to become a declared class rather than
a called function, why every authored read must pass through the Baker to stay
tracked, and why an untracked read is the same category of silent failure as a
false aliasing or [ReadOnly] promise. Next: what happens to that output — the
entity scene, and why baked data loads as a stream of bytes rather than a
sequence of CreateEntity calls.