DOTS//CORE local · not synced

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.

cache line · 64 B fetched 20 B useful · 31% of the bandwidth you paid for a baker's recorded dependency set: the small, explicit list of authored values this one conversion actually read — the thing that makes incremental re-baking decidable

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 chunk space is throughput. An entity that never moves and is never queried by position does not need a 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.

m05.l02