<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Posts on Coyote Time</title>
        <link>https://blog.jonmikeys.com/posts/</link>
        <description>Recent content in Posts on Coyote Time</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-us</language>
        <lastBuildDate>Thu, 15 Oct 2026 00:00:00 +0000</lastBuildDate>
        <atom:link href="https://blog.jonmikeys.com/posts/index.xml" rel="self" type="application/rss+xml" />
        
        <item>
            <title>Part 1: What Tactical Terrain Actually Means</title>
            <link>https://blog.jonmikeys.com/posts/what-tactical-terrain-actually-means/</link>
            <pubDate>Sun, 21 Jun 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/what-tactical-terrain-actually-means/</guid>
            <description>Tactical terrain analysis is the practice of asking what the map makes possible.
A hill, wall, chokepoint, or dark corner is not automatically useful. It becomes useful when it changes movement, visibility, or risk. That is why terrain analysis matters: it turns the map into data that an AI can evaluate.
The basic question The core question is simple:
Which parts of this map give one side an advantage?
That question is more useful than a hand-authored list of “important” spots.</description>
            <content type="html"><![CDATA[<p>Tactical terrain analysis is the practice of asking what the map makes possible.</p>
<p>A hill, wall, chokepoint, or dark corner is not automatically useful. It becomes useful when it changes movement, visibility, or risk. That is why terrain analysis matters: it turns the map into data that an AI can evaluate.</p>
<p><img src="/images/posts/what-tactical-terrain-actually-means/diagram.svg" alt="What Tactical Terrain Actually Means diagram"></p>
<h2 id="the-basic-question">The basic question</h2>
<p>The core question is simple:</p>
<blockquote>
<p>Which parts of this map give one side an advantage?</p>
</blockquote>
<p>That question is more useful than a hand-authored list of “important” spots. Hard-coded regions are fragile. They break when the map changes, when the level is procedural, or when the tactical situation shifts during play.</p>
<p>A better approach is to measure the map directly.</p>
<h2 id="static-and-dynamic-points-of-interest">Static and dynamic points of interest</h2>
<p>Tactical value comes from two kinds of features:</p>
<ul>
<li><strong>static features</strong> exist in the map itself</li>
<li><strong>dynamic features</strong> appear because of the current battle state</li>
</ul>
<p>Static features include cliffs, water, walls, edges of the map, natural chokepoints, and built structures.</p>
<p>Dynamic features include the front line, a retreat route, a newly opened flank, or a region that has become dangerous after unit movement.</p>
<p>The difference matters because a tactically useful location is not useful in isolation. It is useful relative to the current game state.</p>
<h2 id="rts-examples-controlling-movement">RTS examples: controlling movement</h2>
<p>In an RTS, terrain usually matters because it controls movement and commitment.</p>
<p>A chokepoint forces units through a narrow route. That gives defenders a chance to concentrate force. A cliff or wall changes the paths an army can take. An ambush site gives one side a timing or positioning advantage. A flanking route breaks a straight-line engagement by creating another angle of attack.</p>
<p>The common pattern is control. Good terrain makes enemy movement more predictable.</p>
<h2 id="fps-examples-controlling-visibility">FPS examples: controlling visibility</h2>
<p>In a shooter, terrain usually matters because it changes what can be seen and what can be hit.</p>
<p>Cover points reduce exposure. Sniper positions trade safety for sightlines. Escape routes matter because survival often depends on leaving a bad position quickly. Dark areas and low-visibility paths can reduce risk during movement.</p>
<p>A good spot in an FPS is rarely just “high ground.” It usually combines protection, visibility, and a way out.</p>
<h2 id="what-makes-a-location-tactically-useful">What makes a location tactically useful</h2>
<p>Across both genres, the same properties come up repeatedly:</p>
<ul>
<li>control</li>
<li>visibility</li>
<li>protection</li>
<li>accessibility</li>
<li>cost</li>
</ul>
<p>A tactically useful place does one or more of these things well. It forces movement, improves line of sight, reduces exposure, or creates an efficient route.</p>
<p>That is a more reliable model than treating “high ground” as a universal answer.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Tactical terrain analysis is not about memorizing special map locations.</p>
<p>It is about identifying which parts of the map change the odds of a fight.</p>
<p>Once that idea is clear, the next step is easier: store visibility and exposure in the navigation data so the AI can use it directly.</p>
<p>That is where waypoint reasoning starts to become tactical reasoning.</p>
]]></content>
        </item>
        
        <item>
            <title>Part 2: When Waypoints Learn About Visibility</title>
            <link>https://blog.jonmikeys.com/posts/when-waypoints-learn-about-visibility/</link>
            <pubDate>Sun, 21 Jun 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/when-waypoints-learn-about-visibility/</guid>
            <description>A waypoint graph is useful for movement. It becomes more valuable when it also models exposure.
If each node knows what it can see, the graph can answer tactical questions, not just pathfinding questions.
Visibility is the tactical signal Visibility tells you several things at once:
whether a position is exposed whether it provides cover whether it is good for attacking from whether a route is safe to travel whether the position is likely to attract return fire That makes visibility one of the strongest signals in tactical AI.</description>
            <content type="html"><![CDATA[<p>A waypoint graph is useful for movement. It becomes more valuable when it also models exposure.</p>
<p>If each node knows what it can see, the graph can answer tactical questions, not just pathfinding questions.</p>
<p><img src="/images/posts/when-waypoints-learn-about-visibility/diagram.svg" alt="When Waypoints Learn About Visibility diagram"></p>
<h2 id="visibility-is-the-tactical-signal">Visibility is the tactical signal</h2>
<p>Visibility tells you several things at once:</p>
<ul>
<li>whether a position is exposed</li>
<li>whether it provides cover</li>
<li>whether it is good for attacking from</li>
<li>whether a route is safe to travel</li>
<li>whether the position is likely to attract return fire</li>
</ul>
<p>That makes visibility one of the strongest signals in tactical AI. It is cheap to reason about and rich in meaning.</p>
<h2 id="precomputing-node-visibility">Precomputing node visibility</h2>
<p>The standard approach is to preprocess the terrain and store visibility information on waypoint nodes.</p>
<p>Each node can keep a compact record of which other nodes it can see. Bit strings work well for that representation because set operations are fast:</p>
<ul>
<li><strong>and</strong> for overlap</li>
<li><strong>or</strong> for union</li>
<li><strong>not</strong> for exclusion</li>
</ul>
<p>With that data, the AI can compare whole groups of nodes instead of doing expensive reasoning during combat.</p>
<h2 id="safe-and-dangerous-nodes">Safe and dangerous nodes</h2>
<p>Once visibility is stored, nodes can be scored by risk.</p>
<p>A node that is visible to enemies is more dangerous. A hidden node is usually safer. The useful part is not a strict safe/unsafe label. The useful part is a cost model.</p>
<p>That allows the pathfinder to do something practical:</p>
<ul>
<li>prefer low-exposure paths</li>
<li>penalize dangerous nodes</li>
<li>still allow risky movement when needed</li>
</ul>
<p>That is much better than pretending exposed ground is always forbidden.</p>
<h2 id="attack-positioning">Attack positioning</h2>
<p>The same data can be used to choose attack positions.</p>
<p>A good attack node is not just visible to the target. It is also hidden from other threats when possible.</p>
<p>A simple rule looks like this:</p>
<ol>
<li>find nodes visible to the target</li>
<li>remove nodes visible to other enemies</li>
<li>keep the remaining nodes as safer attack positions</li>
</ol>
<p>This is a strong pattern because it turns a fuzzy tactical idea into a filter.</p>
<h2 id="flanking-and-sneaking">Flanking and sneaking</h2>
<p>Flanking is not a special case. It is a visibility problem.</p>
<p>A flank route is useful because it changes who can see whom. Sneaking works the same way: stay out of sight for as long as possible, and arrive from a direction that creates advantage.</p>
<p>That is why visibility data belongs in the navigation layer.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Waypoint graphs become tactical when they stop describing only movement and start describing exposure.</p>
<p>That shift makes the AI better at choosing routes, selecting attack positions, and avoiding unnecessary risk.</p>
<p>The next layer is broader than visibility. It measures pressure across the whole map.</p>
]]></content>
        </item>
        
        <item>
            <title>Part 3: Influence Maps: Turning the Fight Into a Heatmap</title>
            <link>https://blog.jonmikeys.com/posts/influence-maps-turning-the-fight-into-a-heatmap/</link>
            <pubDate>Sun, 21 Jun 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/influence-maps-turning-the-fight-into-a-heatmap/</guid>
            <description>Visibility tells you whether a place can be seen.
Influence maps tell you how much pressure exists there.
That difference matters. A position can be visible without being dangerous, and dangerous without being fully visible. Influence maps help estimate the shape of the fight instead of only the geometry of the map.
What an influence map is An influence map is a grid layered over the terrain. Each cell stores a value that represents power, danger, control, or importance.</description>
            <content type="html"><![CDATA[<p>Visibility tells you whether a place can be seen.</p>
<p>Influence maps tell you how much pressure exists there.</p>
<p>That difference matters. A position can be visible without being dangerous, and dangerous without being fully visible. Influence maps help estimate the shape of the fight instead of only the geometry of the map.</p>
<p><img src="/images/posts/influence-maps-turning-the-fight-into-a-heatmap/diagram.svg" alt="Influence Maps Turning The Fight Into A Heatmap diagram"></p>
<h2 id="what-an-influence-map-is">What an influence map is</h2>
<p>An influence map is a grid layered over the terrain. Each cell stores a value that represents power, danger, control, or importance.</p>
<p>The value usually comes from units or other sources, and it falls off with distance. Nearby cells matter more than distant ones.</p>
<p>That gives the AI a smooth tactical field to reason about.</p>
<h2 id="what-the-map-helps-with">What the map helps with</h2>
<p>Influence maps are useful when the AI needs to answer questions like:</p>
<ul>
<li>where is the strongest enemy presence?</li>
<li>where is the weakest point in the line?</li>
<li>what route avoids the center of the fight?</li>
<li>where is a good flank?</li>
<li>where is the front of the battle?</li>
</ul>
<p>The map does not give perfect answers. It gives a good estimate. That is often enough for decision-making.</p>
<h2 id="combining-multiple-sources">Combining multiple sources</h2>
<p>Influence is rarely coming from one source.</p>
<p>Friendly units, enemy units, objectives, and terrain features can all contribute. The final map is a combination of those signals.</p>
<p>That is what makes the technique useful in both combat and non-combat systems.</p>
<p>The same idea can model crime, pollution, police coverage, fire coverage, or traffic pressure. Any system with spatial force or pressure can use the pattern.</p>
<h2 id="why-it-is-useful">Why it is useful</h2>
<p>Influence maps sit between exact pathfinding and high-level strategy.</p>
<p>Pathfinding finds a route.
Influence maps help decide whether that route is smart.</p>
<p>That makes them a strong middle layer for tactical AI.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Use influence maps when the AI needs a broad read on the battlefield.</p>
<p>They are not a substitute for line-of-sight checks or pathfinding. They are a way to estimate where the fight is strong, weak, safe, or dangerous.</p>
<p>The next step is to store more than one tactical signal at once.</p>
]]></content>
        </item>
        
        <item>
            <title>Part 4: Spatial Databases for Tactical AI</title>
            <link>https://blog.jonmikeys.com/posts/spatial-databases-for-tactical-ai/</link>
            <pubDate>Sun, 21 Jun 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/spatial-databases-for-tactical-ai/</guid>
            <description>A single tactical score is easy to compute and hard to trust.
A layered spatial database is easier to debug because each layer answers one question.
Why layers matter A tactical AI usually needs more than one kind of spatial information.
One layer can describe visibility. Another can describe cover. Another can describe occupancy or line of fire. Keeping those signals separate makes the system easier to reason about.
That is the main idea behind the spatial database in the lecture: represent the map as a stack of tactical views.</description>
            <content type="html"><![CDATA[<p>A single tactical score is easy to compute and hard to trust.</p>
<p>A layered spatial database is easier to debug because each layer answers one question.</p>
<p><img src="/images/posts/spatial-databases-for-tactical-ai/diagram.svg" alt="Spatial Databases For Tactical Ai diagram"></p>
<h2 id="why-layers-matter">Why layers matter</h2>
<p>A tactical AI usually needs more than one kind of spatial information.</p>
<p>One layer can describe visibility. Another can describe cover. Another can describe occupancy or line of fire. Keeping those signals separate makes the system easier to reason about.</p>
<p>That is the main idea behind the spatial database in the lecture: represent the map as a stack of tactical views.</p>
<h2 id="common-layers">Common layers</h2>
<p>The slides describe several useful layers:</p>
<ul>
<li>visibility</li>
<li>openness</li>
<li>cover</li>
<li>area occupancy</li>
<li>line of fire</li>
<li>light level</li>
<li>area search</li>
</ul>
<p>Each layer answers a specific question.</p>
<ul>
<li><strong>visibility</strong>: how exposed is this area?</li>
<li><strong>openness</strong>: how much room is available?</li>
<li><strong>cover</strong>: can an AI survive here?</li>
<li><strong>occupancy</strong>: how crowded is the area?</li>
<li><strong>line of fire</strong>: where are weapons aimed?</li>
<li><strong>light level</strong>: is the area bright or dark?</li>
<li><strong>area search</strong>: has the area already been checked?</li>
</ul>
<p>That separation is important. It keeps the model inspectable.</p>
<h2 id="combining-the-layers">Combining the layers</h2>
<p>The lecture uses a combat desirability layer as an example.</p>
<p>That layer combines signals such as openness, cover, and occupancy into one score.</p>
<p>The exact formula matters less than the design principle: multiple tactical signals should be combined carefully, not smashed into one arbitrary number.</p>
<p>The geometric mean is a good warning here. It prevents one good factor from hiding several bad ones.</p>
<h2 id="why-this-is-better-than-a-black-box">Why this is better than a black box</h2>
<p>If the AI chooses a bad position, a layered system makes it possible to find out why.</p>
<p>Was the area too exposed?</p>
<p>Was it crowded?</p>
<p>Was there too much line-of-fire pressure?</p>
<p>A single hidden score cannot answer those questions. Separate layers can.</p>
<p>That is valuable for debugging and for balancing the AI.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Spatial databases make tactical AI easier to extend because each layer stays focused on one signal.</p>
<p>That makes the system more readable, easier to debug, and less likely to collapse into one opaque heuristic.</p>
<p>The final step is to turn this into a simple implementation plan.</p>
]]></content>
        </item>
        
        <item>
            <title>Part 5: A Practical Tactical Awareness System, Starting Small</title>
            <link>https://blog.jonmikeys.com/posts/practical-tactical-awareness-system-starting-small/</link>
            <pubDate>Sun, 21 Jun 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/practical-tactical-awareness-system-starting-small/</guid>
            <description>A tactical awareness system should start small.
The first version does not need to model everything. It needs to answer one or two useful questions clearly.
Start with one signal The first step is to pick one tactical signal that is easy to measure.
Good candidates are:
visibility cover a small penalty for exposed paths The point is not completeness. The point is to get a result that changes behavior in a useful way.</description>
            <content type="html"><![CDATA[<p>A tactical awareness system should start small.</p>
<p>The first version does not need to model everything. It needs to answer one or two useful questions clearly.</p>
<p><img src="/images/posts/practical-tactical-awareness-system-starting-small/diagram.svg" alt="Practical Tactical Awareness System Starting Small diagram"></p>
<h2 id="start-with-one-signal">Start with one signal</h2>
<p>The first step is to pick one tactical signal that is easy to measure.</p>
<p>Good candidates are:</p>
<ul>
<li>visibility</li>
<li>cover</li>
<li>a small penalty for exposed paths</li>
</ul>
<p>The point is not completeness. The point is to get a result that changes behavior in a useful way.</p>
<h2 id="add-a-second-layer-later">Add a second layer later</h2>
<p>Once the first signal works, add a second one.</p>
<p>That might be:</p>
<ul>
<li>occupancy</li>
<li>danger</li>
<li>line of fire</li>
<li>influence</li>
</ul>
<p>Each new layer should earn its place. If a layer does not improve the decision, it should stay out.</p>
<p>That discipline keeps the system understandable.</p>
<h2 id="keep-the-score-explainable">Keep the score explainable</h2>
<p>A tactical score is only useful if it can be explained.</p>
<p>When the AI chooses a spot, the system should be able to say why:</p>
<ul>
<li>this route is exposed</li>
<li>this area is crowded</li>
<li>this position is visible</li>
<li>this flank is under pressure</li>
</ul>
<p>If the reason is not visible, debugging becomes guesswork.</p>
<h2 id="what-to-avoid">What to avoid</h2>
<p>The main mistake is trying to build a perfect tactical brain on day one.</p>
<p>That usually leads to a system that is hard to tune and hard to trust.</p>
<p>A smaller system with one clear benefit is better than a large system nobody understands.</p>
<h2 id="a-practical-shape-for-version-one">A practical shape for version one</h2>
<p>A useful first version usually looks like this:</p>
<ol>
<li>a small waypoint graph or grid</li>
<li>one tactical score</li>
<li>clear debug output</li>
<li>one or two simple overlays</li>
<li>a gradual path to more layers</li>
</ol>
<p>That is enough to prove the idea without overbuilding it.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Tactical AI works best when it grows in layers.</p>
<p>Start with one reliable signal. Add another only when it improves the model. Keep the decision explainable at every stage.</p>
<p>That is how a tactical system stays useful instead of turning into noise.</p>
]]></content>
        </item>
        
        <item>
            <title>Flyweight and Object Pooling: Share What Repeats, Reuse What Churns</title>
            <link>https://blog.jonmikeys.com/posts/flyweight-and-object-pooling/</link>
            <pubDate>Thu, 15 Oct 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/flyweight-and-object-pooling/</guid>
            <description>Flyweight shares the data that repeats across thousands of objects. Object pooling reuses the instances that keep being created and destroyed. This guide covers ScriptableObject definitions, UnityEngine.Pool, handle-based pools, reset bugs, and when neither is worth it.</description>
            <content type="html"><![CDATA[<p>Flyweight and object pooling show up together in nearly every list of &ldquo;performance patterns,&rdquo; and they get blurred into one idea: avoid doing the expensive thing for every object. That framing is close enough to be dangerous. The two patterns attack different costs, and using the wrong one leaves the original problem untouched while adding new ways to break.</p>
<p>Flyweight is about <strong>memory and bandwidth</strong>. When ten thousand objects carry the same mesh reference, the same stat table, and the same sound list, that data should exist once and be shared.</p>
<p>Object pooling is about <strong>lifetime churn</strong>. When a game spawns and destroys hundreds of short-lived objects per second, such as bullets, hit sparks, damage numbers, and audio sources, the cost is in creating and tearing them down, not in what they hold. A pool keeps a set of instances alive and hands them out again.</p>
<p>Both patterns are old, and both are covered in Robert Nystrom&rsquo;s <em>Game Programming Patterns</em>. This post covers how each one maps onto Unity and C#, where each one fails, and how to combine them so a pooled bullet carries almost no data of its own.</p>
<p><img src="/images/posts/flyweight-and-object-pooling/diagram.svg" alt="Flyweight versus object pool: what each pattern shares or reuses"></p>
<h2 id="two-different-questions">Two different questions</h2>
<p>A quick way to tell which pattern applies is to ask what is expensive.</p>
<p>If the problem is &ldquo;there are too many copies of the same data,&rdquo; the answer is a flyweight. Symptoms: memory grows linearly with object count, inspectors show the same arrays duplicated across prefab instances, or a draw-call count climbs because every object looks like a unique case to the renderer.</p>
<p>If the problem is &ldquo;objects keep being born and dying,&rdquo; the answer is a pool. Symptoms: frame-time spikes during heavy combat, <code>Instantiate</code> and <code>Destroy</code> near the top of the profiler, garbage collection hitches in a profiler capture, or a mobile build that stutters when an effect fires.</p>
<p>Some systems have both problems. A bullet-hell shooter has thousands of bullets that all share a handful of bullet types, and every bullet lives for under two seconds. That is why the patterns pair well, and why it matters to keep them separate in the design. A flyweight that is also doing pool bookkeeping becomes hard to reason about.</p>
<h2 id="flyweight-split-intrinsic-from-extrinsic-state">Flyweight: split intrinsic from extrinsic state</h2>
<p>The classic definition splits an object&rsquo;s data in two.</p>
<p><strong>Intrinsic state</strong> is shared and does not change per instance: the mesh, the base stats, the attack list, the hit sound. <strong>Extrinsic state</strong> is specific to one instance: its position, current health, cooldown timer, and target.</p>
<p>Nystrom&rsquo;s example is a forest of trees. Each tree has a mesh, bark texture, and leaf texture (shared), plus a position, height, and tint (unique). Pulling the shared part into a <code>TreeModel</code> means the mesh exists once in memory rather than a thousand times, and graphics APIs that support instanced rendering can draw the whole forest from one model plus a list of per-instance parameters. He points out that this may be the only Gang of Four pattern with direct hardware support.</p>
<p>In Unity, the natural vehicle for intrinsic state is a <code>ScriptableObject</code>. It is an asset, so every object that references it points at the same instance:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">[CreateAssetMenu(menuName = &#34;Game/Enemy Definition&#34;)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">EnemyDefinition</span> : ScriptableObject
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> maxHealth = <span style="color:#ae81ff">100</span>;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">float</span> moveSpeed = <span style="color:#ae81ff">3.5f</span>;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">float</span> attackRange = <span style="color:#ae81ff">1.8f</span>;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> AudioClip hitSound;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> AnimationCurve damageFalloff;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> MaxHealth =&gt; maxHealth;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> MoveSpeed =&gt; moveSpeed;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> AttackRange =&gt; attackRange;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> AudioClip HitSound =&gt; hitSound;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> AnimationCurve DamageFalloff =&gt; damageFalloff;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The runtime component keeps only what changes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Enemy</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> EnemyDefinition definition;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> health;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">float</span> attackCooldown;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnEnable()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        health = definition.MaxHealth;
</span></span><span style="display:flex;"><span>        attackCooldown = <span style="color:#ae81ff">0f</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> TakeDamage(<span style="color:#66d9ef">int</span> amount)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        health -= amount;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (health &lt;= <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>            Die();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> Die() { <span style="color:#75715e">/* ... */</span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Two properties make this a flyweight rather than a loose bag of settings. The definition is read-only at runtime (private fields, getters only), and the instance never copies definition values into its own fields except for state that must diverge, like current health.</p>
<h3 id="what-the-savings-look-like">What the savings look like</h3>
<p>The saving depends on what the shared data is. A few floats and ints are trivial either way, and a <code>ScriptableObject</code> reference is itself a pointer in every instance. The real gains come from heavy shared payloads: <code>AnimationCurve</code> objects, lists of ability descriptions, localization tables, large config arrays, and anything serialized inline on a prefab.</p>
<p>That last case is easy to miss. If a prefab has a serialized <code>List&lt;AbilityData&gt;</code> on its component, every instance of that prefab carries its own managed copy of the list when instantiated. Move the list into a <code>ScriptableObject</code> and reference it, and the copies collapse into one. Measure first. The Memory Profiler package can show managed object counts and sizes per type, which is a more reliable guide than guessing from the inspector.</p>
<h3 id="rendering-is-the-other-half">Rendering is the other half</h3>
<p>Unity applies the same idea to materials and meshes, and there is a classic trap hiding in it.</p>
<p>Reading <code>renderer.material</code> returns an <strong>instance copy</strong> of the material, creating one if the renderer does not already have its own. Assign a tint through it on a thousand objects and there are a thousand materials, which defeats batching and leaks memory if they are never destroyed. <code>renderer.sharedMaterial</code> returns the shared asset, but writing a property through it changes every object that uses the material.</p>
<p>The flyweight-friendly path is a <code>MaterialPropertyBlock</code>, which carries per-instance overrides without cloning the material:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">TintedRenderer</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">int</span> ColorId = Shader.PropertyToID(<span style="color:#e6db74">&#34;_BaseColor&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> Renderer target;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> MaterialPropertyBlock block;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> SetTint(Color color)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        block ??= <span style="color:#66d9ef">new</span> MaterialPropertyBlock();
</span></span><span style="display:flex;"><span>        target.GetPropertyBlock(block);
</span></span><span style="display:flex;"><span>        block.SetColor(ColorId, color);
</span></span><span style="display:flex;"><span>        target.SetPropertyBlock(block);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is the same split as the forest: one shared material and mesh, plus a small per-instance override. Whether property blocks batch well depends on the render pipeline in use. In pipelines that use the SRP Batcher, per-renderer property blocks can interfere with SRP Batcher compatibility, so check the Frame Debugger before assuming it helped. GPU instancing and the SRP Batcher are different mechanisms with different rules, and the right choice is a rendering-specific question that deserves its own measurement.</p>
<h3 id="the-shared-mutation-bug">The shared-mutation bug</h3>
<p>A flyweight only works while the shared part stays immutable. The most common failure is gameplay code that writes into the definition:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#75715e">// Bug if MoveSpeed were a writable field: every goblin just got faster.</span>
</span></span><span style="display:flex;"><span>definition.MoveSpeed *= <span style="color:#ae81ff">1.2f</span>; <span style="color:#75715e">// does not compile with the read-only API above</span>
</span></span></code></pre></div><p>The read-only properties above are deliberate. Without them, a buff applied to one enemy rewrites the asset in memory, affects every enemy using that definition, and in the editor it can also persist into the saved asset after play mode ends. Buffs belong in extrinsic state: a modifier list on the instance, applied on top of the definition&rsquo;s base values.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> CurrentMoveSpeed =&gt; definition.MoveSpeed * speedMultiplier;
</span></span></code></pre></div><h2 id="object-pooling-reuse-the-instance-not-just-the-data">Object pooling: reuse the instance, not just the data</h2>
<p>A flyweight reduces what each object holds. A pool reduces how often objects are created at all.</p>
<p>Nystrom&rsquo;s definition is a fixed collection of reusable objects, each with an &ldquo;in use&rdquo; flag. Ask the pool for one, and it finds a free object and marks it live. When the object is no longer needed, it goes back to &ldquo;not in use.&rdquo; His motivating examples are particle systems and fragmentation on constrained hardware, where a heap full of small holes can fail an allocation even when plenty of total memory is free.</p>
<p>Unity&rsquo;s situation is a little different from a console-era C++ heap. Fragmentation of the managed heap is real but rarely the first complaint. The usual costs are:</p>
<ul>
<li><code>Instantiate</code> has to clone a hierarchy, run <code>Awake</code> and <code>OnEnable</code> on every component, and register everything with engine systems.</li>
<li><code>Destroy</code> is deferred to the end of the frame and tears the same machinery down.</li>
<li>Any managed allocation created along the way (closures, lists, strings, per-instance arrays) feeds the garbage collector, which can cause a hitch when it runs.</li>
</ul>
<p>Pooling swaps most of that work for activating and deactivating something that already exists. The cost is memory held in reserve and the responsibility to reset state correctly. Neither is free, so both need to be weighed against a measured problem.</p>
<h3 id="the-built-in-pool">The built-in pool</h3>
<p>Since Unity 2021.1, <code>UnityEngine.Pool</code> ships an <code>ObjectPool&lt;T&gt;</code>, so a hand-rolled pool is rarely necessary for GameObjects. The documentation describes it as a stack-based pool whose constructor takes delegates for creation, taking from the pool, returning to the pool, and destroying, plus a default capacity, a maximum size, and an optional collection check that catches double releases.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine.Pool;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ProjectilePool</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> Projectile prefab;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> prewarmCount = <span style="color:#ae81ff">32</span>;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> maxRetained = <span style="color:#ae81ff">128</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> ObjectPool&lt;Projectile&gt; pool;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> Awake()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        pool = <span style="color:#66d9ef">new</span> ObjectPool&lt;Projectile&gt;(
</span></span><span style="display:flex;"><span>            createFunc: CreateProjectile,
</span></span><span style="display:flex;"><span>            actionOnGet: p =&gt; p.gameObject.SetActive(<span style="color:#66d9ef">true</span>),
</span></span><span style="display:flex;"><span>            actionOnRelease: p =&gt; p.gameObject.SetActive(<span style="color:#66d9ef">false</span>),
</span></span><span style="display:flex;"><span>            actionOnDestroy: p =&gt; Destroy(p.gameObject),
</span></span><span style="display:flex;"><span>            collectionCheck: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>            defaultCapacity: prewarmCount,
</span></span><span style="display:flex;"><span>            maxSize: maxRetained);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        Prewarm();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> Projectile CreateProjectile()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Projectile instance = Instantiate(prefab, transform);
</span></span><span style="display:flex;"><span>        instance.gameObject.SetActive(<span style="color:#66d9ef">false</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> instance;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> Prewarm()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> warm = <span style="color:#66d9ef">new</span> Projectile[prewarmCount];
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i = <span style="color:#ae81ff">0</span>; i &lt; warm.Length; i++)
</span></span><span style="display:flex;"><span>            warm[i] = pool.Get();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i = <span style="color:#ae81ff">0</span>; i &lt; warm.Length; i++)
</span></span><span style="display:flex;"><span>            pool.Release(warm[i]);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Projectile Fire(ProjectileDefinition def, Vector3 origin, Vector3 direction)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Projectile p = pool.Get();
</span></span><span style="display:flex;"><span>        p.Launch(def, origin, direction, pool);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> p;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Several details here are easy to get wrong.</p>
<p><code>defaultCapacity</code> sizes the internal collection. It does <strong>not</strong> create objects. Without the <code>Prewarm</code> loop, the first burst of fire still instantiates on demand, which is exactly the hitch the pool was meant to prevent.</p>
<p><code>maxSize</code> limits how many instances the pool <strong>retains</strong>, not how many can be active. If more than <code>maxSize</code> objects are released, the extras go through <code>actionOnDestroy</code>. Calling <code>Get</code> on an empty pool always creates a new instance. If a hard cap on active objects matters (a rule such as &ldquo;never more than 64 bullets on screen&rdquo;), it has to be enforced outside the pool.</p>
<p><code>collectionCheck: true</code> throws on a double release. The check amounts to a linear search of the pooled items, so on a pool that retains hundreds of objects it is not free. Treat it as a development aid and confirm with a profile before leaving it on in a hot path.</p>
<p>The Unity documentation also notes that <code>ObjectPool&lt;T&gt;</code> is not safe to call from background threads and that its objects are stored in a stack-like collection, so there is no guarantee about contiguity in memory. The pool saves allocation work, not cache misses.</p>
<h3 id="the-pooled-object-owns-its-return-trip">The pooled object owns its return trip</h3>
<p>The projectile needs a way back to the pool. Passing the pool in at launch avoids a global lookup:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine.Pool;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Projectile</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> Rigidbody body;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> TrailRenderer trail;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> IObjectPool&lt;Projectile&gt; owner;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> ProjectileDefinition definition;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">float</span> timeLeft;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">bool</span> released;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Launch(
</span></span><span style="display:flex;"><span>        ProjectileDefinition def,
</span></span><span style="display:flex;"><span>        Vector3 origin,
</span></span><span style="display:flex;"><span>        Vector3 direction,
</span></span><span style="display:flex;"><span>        IObjectPool&lt;Projectile&gt; pool)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        definition = def;
</span></span><span style="display:flex;"><span>        owner = pool;
</span></span><span style="display:flex;"><span>        timeLeft = def.Lifetime;
</span></span><span style="display:flex;"><span>        released = <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        transform.SetPositionAndRotation(origin, Quaternion.LookRotation(direction));
</span></span><span style="display:flex;"><span>        body.linearVelocity = direction.normalized * def.Speed; <span style="color:#75715e">// `velocity` before Unity 6</span>
</span></span><span style="display:flex;"><span>        body.angularVelocity = Vector3.zero;
</span></span><span style="display:flex;"><span>        trail.Clear();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> Update()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        timeLeft -= Time.deltaTime;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (timeLeft &lt;= <span style="color:#ae81ff">0f</span>)
</span></span><span style="display:flex;"><span>            Despawn();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnTriggerEnter(Collider other)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// apply damage using definition.Damage ...</span>
</span></span><span style="display:flex;"><span>        Despawn();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> Despawn()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (released)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span>; <span style="color:#75715e">// timeout and hit can land in the same frame</span>
</span></span><span style="display:flex;"><span>        released = <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>        owner.Release(<span style="color:#66d9ef">this</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>released</code> flag matters. A projectile can time out and hit something in the same frame, and releasing twice puts the same object in the pool twice. Then two callers get the same instance, and the symptoms are strange: a bullet teleports mid-flight, or two enemies get hit by one shot. With <code>collectionCheck</code> on, the second release throws instead, which is the better failure.</p>
<h2 id="the-reset-checklist-is-the-real-cost">The reset checklist is the real cost</h2>
<p>A pooled object is a used object. It remembers everything its previous life did, so every bug in a pool is some form of stale state.</p>
<p>The things worth checking each time something goes back in:</p>
<ul>
<li><strong>Physics state.</strong> Velocity, angular velocity, and any forces applied. A reused rigidbody that keeps its last velocity launches in the wrong direction.</li>
<li><strong>Trails and particles.</strong> <code>TrailRenderer.Clear()</code> removes the streak between the old position and the new one. Particle systems need <code>Stop</code> or <code>Clear</code> before reuse, or the previous burst replays.</li>
<li><strong>Event subscriptions.</strong> A pooled object that subscribes in <code>Launch</code> and never unsubscribes responds to events for owners it no longer has. Pair every subscribe with an unsubscribe in the release path.</li>
<li><strong>Running coroutines and async work.</strong> A coroutine started in the last life continues in the next one unless it is stopped. Cancellation tokens tied to the object&rsquo;s active lifetime handle this more reliably than flags.</li>
<li><strong>Timers and cooldowns.</strong> Reset all of them on spawn, not just the obvious one.</li>
<li><strong>Parenting and scale.</strong> Something attached to a moving parent during its last life may still be a child.</li>
<li><strong>Visual state.</strong> Tint overrides, animation state, and enabled sub-objects (a charged-shot glow that was on when the object died).</li>
</ul>
<p>The cleanest approach is to make initialization a single method that sets everything, as <code>Launch</code> does above, rather than scattering resets across <code>OnEnable</code>, <code>OnDisable</code>, and the caller. If a new field gets added to the component, there is exactly one place to update.</p>
<p>Using <code>OnEnable</code> and <code>OnDisable</code> as the lifecycle hooks is tempting because <code>SetActive</code> triggers them. It works, but it hides ordering: <code>OnEnable</code> runs before <code>Launch</code> supplies the definition. Anything that needs the definition has to wait for <code>Launch</code>.</p>
<h2 id="a-handle-based-pool-for-data-that-is-not-a-gameobject">A handle-based pool for data that is not a GameObject</h2>
<p>GameObject pooling is the common case, but the pattern is more general, and a pure data pool is often the better design for very large counts. Ten thousand bullets rarely need ten thousand GameObjects. They need positions, velocities, and a lifetime, which can live in arrays and be rendered by instancing.</p>
<p>That design has a hazard. If a system holds a plain index into the array, and the slot is released and reused, the old index now silently refers to a different bullet. The standard fix is a <strong>generation counter</strong>: each slot has a number that increments on release, and the handle stores both the index and the generation it was issued with.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">BulletHandle</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">int</span> Index;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">int</span> Generation;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> BulletHandle(<span style="color:#66d9ef">int</span> index, <span style="color:#66d9ef">int</span> generation)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Index = index;
</span></span><span style="display:flex;"><span>        Generation = generation;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BulletPool</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> BulletDefinition[] definitions;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> Vector3[] positions;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> Vector3[] velocities;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">float</span>[] timeLeft;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">int</span>[] generations;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">bool</span>[] alive;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">int</span>[] freeSlots;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> freeCount;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> BulletPool(<span style="color:#66d9ef">int</span> capacity)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        definitions = <span style="color:#66d9ef">new</span> BulletDefinition[capacity];
</span></span><span style="display:flex;"><span>        positions = <span style="color:#66d9ef">new</span> Vector3[capacity];
</span></span><span style="display:flex;"><span>        velocities = <span style="color:#66d9ef">new</span> Vector3[capacity];
</span></span><span style="display:flex;"><span>        timeLeft = <span style="color:#66d9ef">new</span> <span style="color:#66d9ef">float</span>[capacity];
</span></span><span style="display:flex;"><span>        generations = <span style="color:#66d9ef">new</span> <span style="color:#66d9ef">int</span>[capacity];
</span></span><span style="display:flex;"><span>        alive = <span style="color:#66d9ef">new</span> <span style="color:#66d9ef">bool</span>[capacity];
</span></span><span style="display:flex;"><span>        freeSlots = <span style="color:#66d9ef">new</span> <span style="color:#66d9ef">int</span>[capacity];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i = <span style="color:#ae81ff">0</span>; i &lt; capacity; i++)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            generations[i] = <span style="color:#ae81ff">1</span>; <span style="color:#75715e">// 0 is reserved so default(BulletHandle) is never valid</span>
</span></span><span style="display:flex;"><span>            freeSlots[i] = capacity - <span style="color:#ae81ff">1</span> - i;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        freeCount = capacity;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">bool</span> TryFire(
</span></span><span style="display:flex;"><span>        BulletDefinition def,
</span></span><span style="display:flex;"><span>        Vector3 origin,
</span></span><span style="display:flex;"><span>        Vector3 direction,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">out</span> BulletHandle handle)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (freeCount == <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            handle = <span style="color:#66d9ef">default</span>;
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>; <span style="color:#75715e">// caller decides: drop the shot, or recycle the oldest</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">int</span> i = freeSlots[--freeCount];
</span></span><span style="display:flex;"><span>        definitions[i] = def;
</span></span><span style="display:flex;"><span>        positions[i] = origin;
</span></span><span style="display:flex;"><span>        velocities[i] = direction.normalized * def.Speed;
</span></span><span style="display:flex;"><span>        timeLeft[i] = def.Lifetime;
</span></span><span style="display:flex;"><span>        alive[i] = <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        handle = <span style="color:#66d9ef">new</span> BulletHandle(i, generations[i]);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">bool</span> IsAlive(BulletHandle h) =&gt;
</span></span><span style="display:flex;"><span>        h.Index &gt;= <span style="color:#ae81ff">0</span> &amp;&amp; h.Index &lt; alive.Length &amp;&amp;
</span></span><span style="display:flex;"><span>        alive[h.Index] &amp;&amp; generations[h.Index] == h.Generation;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Tick(<span style="color:#66d9ef">float</span> dt)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i = <span style="color:#ae81ff">0</span>; i &lt; alive.Length; i++)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> (!alive[i])
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">continue</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            positions[i] += velocities[i] * dt;
</span></span><span style="display:flex;"><span>            timeLeft[i] -= dt;
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> (timeLeft[i] &lt;= <span style="color:#ae81ff">0f</span>)
</span></span><span style="display:flex;"><span>                Release(i);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> Release(<span style="color:#66d9ef">int</span> i)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        alive[i] = <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>        definitions[i] = <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>        generations[i]++;
</span></span><span style="display:flex;"><span>        freeSlots[freeCount++] = i;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This pool allocates once in the constructor and never again. <code>TryFire</code> returns <code>false</code> when full, which forces the caller to pick one of the overflow strategies Nystrom lists: size the pool so it never overflows, skip the new object, kill an existing one, or grow the pool. For cosmetic bullets, skipping is usually right. For a boss spawn, the pool must be sized so the question never comes up.</p>
<p>Notice what each slot stores: a reference to a shared <code>BulletDefinition</code> and four small pieces of per-bullet state. That is the combined design. The flyweight holds speed, damage, lifetime, mesh, and material. The pool holds a slot, a handle, and nothing else. Neither pattern had to be bent to make this work.</p>
<p>A short check is worth keeping beside code like this, because generation bugs are quiet:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> pool = <span style="color:#66d9ef">new</span> BulletPool(<span style="color:#ae81ff">2</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> def = ScriptableObject.CreateInstance&lt;BulletDefinition&gt;();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>pool.TryFire(def, Vector3.zero, Vector3.forward, <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> first);
</span></span><span style="display:flex;"><span>pool.Tick(def.Lifetime + <span style="color:#ae81ff">0.1f</span>);                       <span style="color:#75715e">// first expires, slot is freed</span>
</span></span><span style="display:flex;"><span>pool.TryFire(def, Vector3.zero, Vector3.forward, <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> second);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Debug.Assert(!pool.IsAlive(first));   <span style="color:#75715e">// stale handle must not alias the reused slot</span>
</span></span><span style="display:flex;"><span>Debug.Assert(pool.IsAlive(second));
</span></span></code></pre></div><p>If <code>IsAlive(first)</code> ever returns true after the slot is reused, the generation logic is broken, and the symptom in-game would be damage applied to the wrong bullet.</p>
<h2 id="sizing-and-overflow">Sizing and overflow</h2>
<p>Pool size is a gameplay decision wearing an engineering costume.</p>
<p>Too small, and shots silently vanish or objects are created mid-fight, which is the hitch the pool existed to prevent. Too large, and the memory sits idle, which matters on mobile and console budgets. Nystrom&rsquo;s advice is blunt: a crash makes an undersized pool obvious, so also watch for oversized ones, because they cost memory that something else could use.</p>
<p>A practical approach:</p>
<ol>
<li>Run a worst-case scenario (the heaviest encounter, the highest fire rate, all upgrades active).</li>
<li>Record the peak number of simultaneously active objects. Log it from the pool during play, which costs one integer comparison per spawn.</li>
<li>Set the prewarm count near typical peak and the retained cap near the worst-case peak plus a small margin.</li>
<li>Keep the logging in development builds so later balance changes that raise the peak get noticed.</li>
</ol>
<p>Separate pools per object type also help. One shared pool for &ldquo;effects&rdquo; lets a flood of sparks starve a more important effect. Partitioning memory by purpose is a feature, not a limitation.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<p><strong>Pooling without profiling.</strong> If a prefab spawns a few times per minute, a pool adds code, memory, and reset bugs for no measurable gain. Capture a profile with the problem present, confirm that <code>Instantiate</code>, <code>Destroy</code>, or GC allocation is the cost, and then pool the specific objects responsible.</p>
<p><strong>Forgetting that <code>SetActive(false)</code> still costs memory.</strong> Pooled objects stay resident. A pool of two hundred complex enemy prefabs holds all of their components, meshes in use, and audio sources even when none are on screen.</p>
<p><strong>Resetting in the wrong place.</strong> If reset logic is split among the pool, the caller, and several components, one will be missed. Centralize it.</p>
<p><strong>Mutating the flyweight.</strong> Anything writing to a shared definition at runtime changes every object that uses it. Keep definitions read-only and layer modifiers on the instance.</p>
<p><strong>Holding raw references to pooled objects.</strong> A system that stores a <code>Projectile</code> reference after the projectile returned to the pool may now be talking to a different bullet. Use handles with generations, or clear references in the release path.</p>
<p><strong>Treating a pool as a fix for allocation inside the object.</strong> Pooling a <code>Projectile</code> does nothing if its <code>Update</code> allocates a list or a closure every frame. Fix the per-frame allocation separately.</p>
<p><strong>Using both patterns where one is enough.</strong> A pool of identical, tiny objects does not need a definition asset. A set of long-lived, unique enemies does not need a pool. Match the pattern to the cost that exists.</p>
<h2 id="when-to-skip-both">When to skip both</h2>
<p>Flyweight is not worth the indirection when there are only a few instances. Ten enemies with their own copies of a stat block cost nothing that matters, and a per-instance setup is easier to tweak in the inspector.</p>
<p>Pooling is not worth the reset burden for objects that are expensive but rare, like a boss, a level transition, or a UI screen opened a few times a session. Those costs are better hidden behind a loading screen or spread across frames with async loading than paid for in permanent memory.</p>
<p>There is also a middle ground for arrays and lists. <code>ArrayPool&lt;T&gt;</code> from <code>System.Buffers</code> and the collection pools in <code>UnityEngine.Pool</code> (such as <code>ListPool&lt;T&gt;</code>) cover temporary buffers, so code that builds a list every frame can borrow and return one without writing a custom pool.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Treat the two patterns as answers to two questions. If many objects carry identical data, move that data into a read-only shared definition and keep only per-instance state on the object. If many objects are created and destroyed quickly, keep a bounded set of them alive and recycle them through a single reset path.</p>
<p>When using Unity&rsquo;s <code>ObjectPool&lt;T&gt;</code>, prewarm explicitly, enforce the active-object cap outside the pool, guard against double release, and decide up front what happens when the pool runs dry. For data-oriented pools, issue handles with a generation counter so stale references fail loudly instead of pointing at a stranger.</p>
<p>Then measure. Both patterns trade memory and complexity for speed, and that trade is only worth making when a profile shows the cost it removes.</p>
<h2 id="references">References</h2>
<ul>
<li>Robert Nystrom, <em>Game Programming Patterns</em>, <a href="https://gameprogrammingpatterns.com/flyweight.html">Flyweight</a> — the forest example, the intrinsic/extrinsic split, and the note about hardware instancing.</li>
<li>Robert Nystrom, <em>Game Programming Patterns</em>, <a href="https://gameprogrammingpatterns.com/object-pool.html">Object Pool</a> — fragmentation, fixed-size trade-offs, overflow strategies, and the warning that reused objects are not cleared for you.</li>
<li>Unity Scripting API, <a href="https://docs.unity3d.com/6000.0/Documentation/ScriptReference/Pool.ObjectPool_1.html">ObjectPool&lt;T&gt;</a> — constructor parameters, <code>maxSize</code> behavior, collection checks, and the thread-safety note.</li>
<li>Unity Scripting API, <a href="https://docs.unity3d.com/6000.0/Documentation/ScriptReference/MaterialPropertyBlock.html">MaterialPropertyBlock</a> — per-renderer overrides without cloning materials.</li>
<li>Unity Manual, <a href="https://docs.unity3d.com/6000.0/Documentation/Manual/class-ScriptableObject.html">ScriptableObject</a> — using assets to hold shared data and reduce per-instance copies.</li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Mediator Pattern in Games: One Middleman Instead of a Web of Direct Dependencies</title>
            <link>https://blog.jonmikeys.com/posts/mediator-pattern-in-games/</link>
            <pubDate>Sun, 11 Oct 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/mediator-pattern-in-games/</guid>
            <description>A practical guide to the mediator pattern in game code: when a group of components needs to coordinate, and one scoped middleman beats a web of direct references.</description>
            <content type="html"><![CDATA[<p>The mediator pattern is what I reach for when several parts of one feature have to stay in sync, and wiring them to each other directly is turning the scene graph into a ball of string.</p>
<p>It sounds abstract until you have lived it. A character-select screen has a portrait list, a preview model, a stat panel, a ready button, a team summary, and a lobby connection. Clicking a portrait changes the preview. Changing the preview changes the stats. The ready button depends on the selection being valid. The lobby needs to know when everyone is ready. If every widget holds a reference to every other widget, adding a single new panel means threading one more dependency through the whole pile.</p>
<p>The mediator offers a different shape. Colleagues do not know about each other. They talk to one object in the middle, and that object decides who else needs to hear about it.</p>
<p>That is the whole pattern in a sentence. The difficulty, as usual, is deciding where the middle object should live and how much it is allowed to know.</p>
<p><img src="/images/posts/mediator-pattern-in-games/diagram.svg" alt="Mediator hub-and-spoke flow"></p>
<h2 id="the-shape-of-the-pattern">The shape of the pattern</h2>
<p>A mediator has two kinds of participant.</p>
<p>The <strong>colleagues</strong> are the components that need to coordinate. They do the actual work: rendering a portrait, computing stats, applying a preview, sending a lobby message. Each colleague knows about the mediator, but not about the other colleagues.</p>
<p>The <strong>mediator</strong> knows about all of its colleagues and owns the rules for how they coordinate. When a colleague reports a change, the mediator decides what that change means and tells the affected colleagues.</p>
<p>In C#, a minimal version looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IMediator</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Notify(<span style="color:#66d9ef">object</span> sender, <span style="color:#66d9ef">string</span> message);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SelectionMediator</span> : IMediator
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> PortraitList? portraits;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> StatPanel? stats;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> PreviewModel? preview;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> ReadyButton? ready;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Register(<span style="color:#66d9ef">object</span> component)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">switch</span> (component)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">case</span> PortraitList p: portraits = p; <span style="color:#66d9ef">break</span>;
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">case</span> StatPanel s: stats = s; <span style="color:#66d9ef">break</span>;
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">case</span> PreviewModel m: preview = m; <span style="color:#66d9ef">break</span>;
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">case</span> ReadyButton r: ready = r; <span style="color:#66d9ef">break</span>;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Notify(<span style="color:#66d9ef">object</span> sender, <span style="color:#66d9ef">string</span> message)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (sender == portraits &amp;&amp; message == <span style="color:#e6db74">&#34;selected&#34;</span>)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            preview?.Show(portraits!.Current);
</span></span><span style="display:flex;"><span>            stats?.Refresh(portraits.Current);
</span></span><span style="display:flex;"><span>            ready?.SetEnabled(portraits.Current != <span style="color:#66d9ef">null</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> (sender == ready &amp;&amp; message == <span style="color:#e6db74">&#34;ready&#34;</span>)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// tell the lobby, not the other widgets</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The colleagues call the mediator when something happens, and the mediator is the only object that knows the full choreography.</p>
<p>Notice what is missing: no colleague holds a reference to another colleague. The portrait list does not know the stat panel exists. The ready button does not reach into the preview model. That is the payoff.</p>
<p>The cost is equally visible: the mediator knows a lot. That is the tradeoff the pattern asks you to accept on purpose, and most of the discipline around mediators is about keeping that knowledge contained.</p>
<h2 id="mediator-is-not-an-event-bus-and-not-just-observer">Mediator is not an event bus, and not just observer</h2>
<p>This is the distinction that causes the most confusion, because all three patterns are about communication without direct references.</p>
<p><strong>Observer</strong> is one-to-many. A subject keeps a list of listeners and notifies them without knowing what they do. The subject is reusable because it does not care about its listeners.</p>
<p><strong>An event bus</strong> is a global broadcast channel. Producers publish, consumers subscribe, and neither side knows the other. The bus is usually process-wide and generic.</p>
<p><strong>A mediator</strong> is a <em>scoped, opinionated</em> object. It does know its colleagues, it does understand the feature, and it makes decisions about the flow. It is closer to a small coordinator or a mini-controller than to a dumb pipe.</p>
<p>The practical difference shows up in where the logic lives. With an event bus, the coordination rules are scattered across the subscribers. When a selection changes, the stat panel subscribes to <code>SelectionChanged</code> and refreshes, the preview subscribes and renders, the ready button subscribes and revalidates. Each listener knows its own slice, and nobody knows the whole flow. That is often what you want for fire-and-forget notifications.</p>
<p>With a mediator, the flow lives in one place. <code>SelectionChanged</code> becomes <code>mediator.Notify(this, &quot;selected&quot;)</code>, and the mediator is the single object that knows &ldquo;a selection change means update preview, then stats, then ready state.&rdquo; If the rules change, you edit the mediator, not six scattered listeners.</p>
<p>Neither is universally better. Event buses scale to many-to-many communication and let systems stay ignorant of each other. Mediators scale to a bounded group that has a real, deliberate workflow. If your &ldquo;mediator&rdquo; has forty colleagues and a stringly-typed message protocol, you have built an event bus with extra steps and should probably just use the bus.</p>
<h2 id="a-concrete-example-turn-based-combat">A concrete example: turn-based combat</h2>
<p>Turn flow is a natural mediator candidate because it has a real sequence with several actors.</p>
<p>A tactics battle needs to coordinate the turn manager, the unit selection input, the action menu, the movement validator, the attack resolver, and the HUD. If the turn manager holds references to all of those, and the HUD holds references back to the turn manager, you get a cycle that is hard to reason about and harder to test.</p>
<p>A <code>BattleMediator</code> can own the sequence:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BattleMediator</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> TurnManager turns;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> UnitSelection selection;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ActionMenu menu;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> MoveValidator moveValidator;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> AttackResolver attackResolver;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> BattleHud hud;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> BattleMediator(
</span></span><span style="display:flex;"><span>        TurnManager turns,
</span></span><span style="display:flex;"><span>        UnitSelection selection,
</span></span><span style="display:flex;"><span>        ActionMenu menu,
</span></span><span style="display:flex;"><span>        MoveValidator moveValidator,
</span></span><span style="display:flex;"><span>        AttackResolver attackResolver,
</span></span><span style="display:flex;"><span>        BattleHud hud)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.turns = turns;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.selection = selection;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.menu = menu;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.moveValidator = moveValidator;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.attackResolver = attackResolver;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.hud = hud;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> OnUnitSelected(UnitId unit)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        menu.ShowFor(unit);
</span></span><span style="display:flex;"><span>        hud.Highlight(unit);
</span></span><span style="display:flex;"><span>        hud.ShowMoveRange(moveValidator.RangeFor(unit));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> OnActionChosen(IAction action)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (action <span style="color:#66d9ef">is</span> MoveAction move)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            menu.Hide();
</span></span><span style="display:flex;"><span>            hud.ShowMoveTargets(moveValidator.TargetsFor(selection.Current, move));
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> (action <span style="color:#66d9ef">is</span> AttackAction attack)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            menu.Hide();
</span></span><span style="display:flex;"><span>            hud.ShowAttackTargets(attackResolver.TargetsFor(selection.Current, attack));
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> OnTargetConfirmed(TargetId target)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> result = attackResolver.Resolve(selection.Current, selection.Action, target);
</span></span><span style="display:flex;"><span>        hud.ShowResult(result);
</span></span><span style="display:flex;"><span>        turns.Advance();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The combat pieces stay focused on their own job. The mediator holds the script of the turn. When a new feature lands — an assist action, a reaction trigger, an undo step — it is one more branch in a class that is already the designated owner of that flow.</p>
<p>That is the healthy version. The unhealthy version is when <code>BattleMediator</code> also starts managing the inventory, the quest log, the save system, and the camera, because it happens to be convenient to route everything through one object. The scope line is the difference between a mediator and a god object.</p>
<h2 id="scope-is-the-whole-discipline">Scope is the whole discipline</h2>
<p>A mediator should own one feature, one screen, or one bounded workflow. The selection mediator owns selection. The battle mediator owns the battle turn. The settings mediator owns the settings screen.</p>
<p>The moment a mediator starts coordinating across two unrelated features, it is accumulating the same global knowledge the pattern was meant to avoid. It has not removed the web; it has rolled the web into a ball and hidden it behind a name.</p>
<p>A useful rule of thumb: if you can describe what the mediator coordinates in one short sentence, it is probably scoped well. &ldquo;Coordinates the character-select screen.&rdquo; &ldquo;Coordinates the battle turn.&rdquo; &ldquo;Coordinates the in-game HUD and pause menu.&rdquo; If the description needs a paragraph and three caveats, the mediator is doing too much and should be split along the features it touches.</p>
<p>Splitting is usually cheap. A <code>LobbyMediator</code> can talk to a <code>SelectionMediator</code> through a narrow interface, the same way a colleague talks to its own mediator. Mediators can be colleagues of a higher-level mediator. The pattern composes, and composing is how you keep any single object from swallowing the project.</p>
<h2 id="keep-messages-typed-and-small">Keep messages typed and small</h2>
<p>The stringly-typed <code>Notify(sender, &quot;selected&quot;)</code> shape is the classic textbook version, and it works for small examples. It is also a magnet for typos and for the &ldquo;who sends which string&rdquo; scavenger hunt that makes mediator code annoying to debug.</p>
<p>In real game code I usually prefer typed messages. Instead of one <code>Notify</code> method with a string, give the mediator explicit methods like <code>OnUnitSelected</code> and <code>OnTargetConfirmed</code> above, or route a small typed event object through it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">struct</span> SelectionChanged(UnitId? Unit);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SelectionMediator</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Handle(SelectionChanged changed)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        preview?.Show(changed.Unit);
</span></span><span style="display:flex;"><span>        stats?.Refresh(changed.Unit);
</span></span><span style="display:flex;"><span>        ready?.SetEnabled(changed.Unit <span style="color:#66d9ef">is</span> not <span style="color:#66d9ef">null</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Typed methods or records buy you compiler-checked wiring and make the flow greppable. The strings buy you nothing except a habit that breaks the first time you rename a message and miss a sender.</p>
<p>The tradeoff is that typed mediators are less generic. A single <code>IMediator</code> with one <code>Notify</code> method can be reused across many features; a typed mediator is written per feature. For a game, the per-feature cost is usually worth it, because game features are bespoke anyway and the coordination rules rarely look identical between them.</p>
<p>Do not build a generic reusable mediator framework before you have at least three concrete mediators that are actually painful to write. Generic message-broker infrastructure is a framework problem, and most game teams do not have it. They have three screens that need a coordinator, which is a much smaller problem.</p>
<h2 id="wiring-it-in-unity">Wiring it in Unity</h2>
<p>There are two common ways to give a mediator its colleagues in Unity, and they map to two different stages of a project.</p>
<p>The simplest is a <code>MonoBehaviour</code> mediator that grabs references in the scene. A character-select screen is a concrete scene layout, so a <code>SelectionScreenMediator : MonoBehaviour</code> with <code>[SerializeField]</code> references to the widgets can be wired in the Inspector and left alone.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SelectionScreenMediator</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> PortraitList portraits;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> StatPanel stats;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> PreviewModel preview;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> ReadyButton ready;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> LobbyClient lobby;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> OnPortraitSelected(UnitId unit)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        preview.Show(unit);
</span></span><span style="display:flex;"><span>        stats.Refresh(unit);
</span></span><span style="display:flex;"><span>        ready.SetEnabled(unit != <span style="color:#66d9ef">null</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> OnReadyPressed()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (portraits.Current <span style="color:#66d9ef">is</span> not UnitId unit) <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>        lobby.SendReady(unit);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is fine for a feature that lives in one scene and is authored by hand. The Inspector wiring is explicit, and the mediator is where a designer or another programmer expects to find the screen logic.</p>
<p>The second way is to inject a plain C# mediator with a composition root or a small DI container. This is better for gameplay flows that are created at runtime and tested without a scene: a battle mediator, a match-flow coordinator, a tutorial script. The mediator becomes an ordinary object you can construct in a test with fake colleagues.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> mediator = <span style="color:#66d9ef">new</span> BattleMediator(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">new</span> TurnManager(),
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">new</span> UnitSelection(),
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">new</span> ActionMenu(fakeInput),
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">new</span> MoveValidator(grid),
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">new</span> AttackResolver(combatRules),
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">new</span> BattleHud(fakeView));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>mediator.OnUnitSelected(unit);
</span></span><span style="display:flex;"><span><span style="color:#75715e">// assert the fake view received a highlight call</span>
</span></span></code></pre></div><p>The pattern does not force either choice. Keep the mediation logic in plain C# when you can, because it is the kind of flow logic that benefits from being testable without booting Unity. Wrap it in a <code>MonoBehaviour</code> only where the scene is the natural owner of the wiring.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<p>The first mistake is treating mediator as a default. Most communication in a game does not need a middleman. A direct call from the player controller to the audio manager is fine. A plain event for &ldquo;player died&rdquo; is fine. Reach for a mediator when there is a real multi-step flow with several actors, not because you feel like the architecture should have one.</p>
<p>The second mistake is letting the mediator grow until it knows everything. A mediator that coordinates selection, combat, quests, saves, and settings is no longer a coordinator; it is the object you were trying to avoid. Scope it to one feature and split it when it bleeds over.</p>
<p>The third mistake is using strings for messages and then losing track of who sends what. Typed methods or small typed events keep the flow readable and compiler-checked. If you must use strings, at least define them as constants in one place and document who sends each one.</p>
<p>The fourth mistake is making the mediator do the colleagues&rsquo; work. The mediator routes and sequences. It should not also compute damage, render portraits, or validate movement. If the mediator contains the feature&rsquo;s actual logic rather than its choreography, the responsibilities have blurred and you should push the work back into the colleague that owns it.</p>
<p>The fifth mistake is trying to make one generic mediator interface cover every feature before you have evidence that it pays off. Concrete, small, per-feature mediators are boring but they are also correct. A framework that generalizes over features you have not built yet is a bet on a problem you do not have.</p>
<h2 id="when-a-mediator-is-the-wrong-tool">When a mediator is the wrong tool</h2>
<p>There are three signals that tell you to use something simpler.</p>
<p>If the coordination is one-way and the sender does not care who listens, use an event or an event bus. A mediator only earns its keep when the flow is deliberate and someone needs to own the whole script.</p>
<p>If the group of components is tiny and stable, direct references may be clearer than a middleman. Two widgets that always change together do not need a coordinator; they can just talk to each other. The pattern is for when the mesh grows and every new widget means several more edges.</p>
<p>If the logic is already owned by a state machine or a controller, a mediator can be redundant. A turn-based battle already has a turn state machine; the mediator might be the state machine with a new name. That is not necessarily wrong, but do not add a second object that does the same job.</p>
<p>The honest way to think about it: a mediator is a tool for keeping <em>many-to-many coordination</em> from becoming a <em>many-to-many reference graph</em>. If you do not have many-to-many coordination, you do not need it.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Use the mediator pattern when a bounded group of components has a real workflow that needs a single owner, and the alternative is every component holding references to every other.</p>
<p>Give the mediator explicit, typed methods or small typed events instead of string messages. Scope it to one feature and split it the moment it starts coordinating unrelated systems. Keep the actual work inside the colleagues and the choreography inside the mediator. In Unity, prefer a plain C# mediator for testable gameplay flows and a scene-bound <code>MonoBehaviour</code> only when the scene is the natural owner of the wiring.</p>
<p>The mediator is not a more powerful observer or a fancier event bus. It is a deliberate decision to centralize one small piece of coordination knowledge in exchange for removing the dependency web around it. Centralize the right piece, keep it small, and the pattern stays boring — which is exactly what you want from a middleman.</p>
<h2 id="references">References</h2>
<ul>
<li>Refactoring.Guru, <a href="https://refactoring.guru/design-patterns/mediator">Mediator</a> — the classic originator/colleague framing and the comparison with observer.</li>
<li>Robert Nystrom, <em>Game Programming Patterns</em>, <a href="https://gameprogrammingpatterns.com/observer.html">Observer</a> and <a href="https://gameprogrammingpatterns.com/event-queue.html">Event Queue</a> — useful adjacent reading for deciding between a mediator and a broadcast channel.</li>
<li>Microsoft Learn, <a href="https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/events/">Events (C# Programming Guide)</a> — the language-level events that often make a mediator unnecessary for simple one-way notifications.</li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Service Locator in Unity: The Global Registry, Used Where It Belongs</title>
            <link>https://blog.jonmikeys.com/posts/service-locator-in-unity/</link>
            <pubDate>Wed, 07 Oct 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/service-locator-in-unity/</guid>
            <description>Service locator gets dismissed as a global with nicer clothes. It has real, defensible uses at engine boundaries and in small games — here is where it earns its keep and where it rots.</description>
            <content type="html"><![CDATA[<p>Most advice about the service locator pattern comes down to one sentence: &ldquo;don&rsquo;t.&rdquo;</p>
<p>That is an oversimplification, and it is worth being honest about why.</p>
<p>The service locator is a real tool with a real cost. It is not secretly good architecture, but it is also not a cardinal sin. The useful question is not &ldquo;should I use a service locator&rdquo; but &ldquo;where does it belong, and what am I trading away when I use it.&rdquo;</p>
<p>This post is a companion to the dependency injection articles already on this site. Those articles make the case for wiring dependencies at the edges. This one makes the case for the exception.</p>
<p><img src="/images/posts/service-locator-in-unity/diagram.svg" alt="Service Locator in Unity diagram"></p>
<h2 id="pull-versus-push">Pull versus push</h2>
<p>Dependency injection and service location are two answers to the same question: how does an object get the collaborators it needs?</p>
<p>Dependency injection is a push model. Something outside the object hands it the dependencies, usually through a constructor or a setup method. The object never asks for anything. It just receives.</p>
<p>A service locator is a pull model. The object reaches into a shared registry and asks for what it wants, when it wants it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#75715e">// Push: the dependency arrives from outside.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerController</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IAudioService audio;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> PlayerController(IAudioService audio)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.audio = audio;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#75715e">// Pull: the object goes and fetches the dependency.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerController</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> IAudioService audio;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Initialize()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        audio = Services.Get&lt;IAudioService&gt;();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Both end with the object holding a reference to <code>IAudioService</code>. The difference is who decided where that reference came from.</p>
<p>In the push version, the caller decides. In the pull version, the object decides, and the only thing it has to know is that a registry exists and contains audio.</p>
<h2 id="the-honest-criticisms">The honest criticisms</h2>
<p>The anti-service-locator arguments are correct. It helps to state them plainly instead of gesturing at them.</p>
<p><strong>The dependency is hidden.</strong> Read a class that uses constructor injection and the signature tells you exactly what it needs. Read a class that pulls from a locator and you have to scan the body for every <code>Services.Get&lt;T&gt;()</code> call to build the same picture. The dependency graph still exists; it is just scattered inside method bodies.</p>
<p><strong>Tests have to set up global state.</strong> A class that pulls from a static locator cannot be tested in isolation. Before the test runs, the registry has to be populated with the right fakes, and after it runs, the registry has to be cleaned up. That is global mutable state, and global mutable state makes tests order-dependent and easy to break.</p>
<p><strong>Errors move to runtime.</strong> Constructor injection fails at construction time. If <code>PlayerController</code> needs audio and audio is missing, you find out when you try to build it. A locator can fail later, at some arbitrary point in a gameplay frame, when the object finally asks for a service that was never registered. The stack trace still points at the <code>Get</code> call, but the distance between the mistake and the failure has grown.</p>
<p><strong>Coupling to the locator itself.</strong> Every class that calls <code>Services.Get&lt;T&gt;()</code> depends on the locator, not just on the service it wants. The locator becomes a hard-to-remove seam running through the whole codebase.</p>
<p>None of these are opinions. They are the mechanical consequences of hiding a dependency behind an ambient lookup. The next question is whether there are places where those costs are worth paying.</p>
<h2 id="why-it-is-still-here">Why it is still here</h2>
<p>If service location were purely harmful, it would have died out decades ago. It has not, because a few situations make the lookup genuinely the lesser evil.</p>
<p><strong>Deep call chains.</strong> Sometimes a service has to travel through ten layers of objects before it reaches the one that uses it. Threading a logger, a profiler marker, or a random number source through every constructor in that chain adds noise to code that has nothing to do with those services. A lookup lets the leaf object grab what it needs without making every intermediate object aware of it.</p>
<p><strong>Ambient services.</strong> Some things are context, not collaboration. A clock, an input source, a settings object, a frame counter. They are everywhere, they rarely change, and forcing every class to declare them makes signatures heavier without making the code clearer. The line between &ldquo;ambient context&rdquo; and &ldquo;service locator&rdquo; is thin, and it is mostly a question of discipline.</p>
<p><strong>Engine boundaries.</strong> This is the Unity-specific case, and it matters most in practice.</p>
<p><strong>Small games and prototypes.</strong> When the whole game is five scenes and three systems, a global registry is fast to build and easy to read. The full dependency-injection apparatus is a tax you do not need to pay yet. The risk is that the small game grows and the registry becomes the default pattern even after it stops being convenient.</p>
<p>The honest position is not &ldquo;never use it.&rdquo; It is &ldquo;know what it costs, and use it where the cost is already being paid for another reason.&rdquo;</p>
<h2 id="why-unity-leans-toward-lookup">Why Unity leans toward lookup</h2>
<p>Unity makes constructor injection awkward in a specific way: the engine constructs your <code>MonoBehaviour</code> components, not your code.</p>
<p>Unity&rsquo;s own documentation warns against giving <code>MonoBehaviour</code> a constructor, because the engine manages those objects and their lifecycle. That closes off the cleanest injection point. You cannot require an <code>IAudioService</code> in the constructor of a component the engine will create.</p>
<p>That leaves three honest options for Unity components:</p>
<ol>
<li><strong>Constructor injection for plain C# classes.</strong> Keep the gameplay logic in ordinary classes that you construct yourself, and inject normally there.</li>
<li><strong>Explicit setup methods.</strong> A thin <code>MonoBehaviour</code> calls <code>Initialize(IAudioService audio, ISaveService save)</code> from a bootstrapper.</li>
<li><strong>Service lookup at the boundary.</strong> The component pulls from a registry, accepting the hidden-dependency cost in exchange for not fighting the engine.</li>
</ol>
<p>The third option is not automatically wrong. It is often the most honest thing to do in a codebase that is mostly Unity components. The trick is to keep the lookup at the boundary and not let it leak into the pure logic.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerHud</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> IAudioService audio;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> Awake()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Boundary lookup: the engine made this object, so it pulls its tools.</span>
</span></span><span style="display:flex;"><span>        audio = Services.Get&lt;IAudioService&gt;();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> OnHealthChanged(<span style="color:#66d9ef">int</span> current, <span style="color:#66d9ef">int</span> max)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        audio.Play(current &lt;= max / <span style="color:#ae81ff">4</span> ? <span style="color:#e6db74">&#34;low-health&#34;</span> : <span style="color:#e6db74">&#34;health-change&#34;</span>);
</span></span><span style="display:flex;"><span>        healthBar.SetFill((<span style="color:#66d9ef">float</span>)current / max);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That component is still mostly glue. The real rules live in plain classes that can take a constructor. The lookup is confined to <code>Awake</code>, at the edge, where the engine already forced your hand.</p>
<h2 id="a-registry-that-fails-fast">A registry that fails fast</h2>
<p>The difference between a tolerable service locator and an awful one is mostly in how it handles the unhappy path.</p>
<p>A locator that returns <code>null</code> for a missing service is the worst version. It pushes the failure into whatever calls the method, usually a null-reference exception deep in a frame, far from the actual mistake.</p>
<p>A locator that throws immediately on a missing registration is far better. It turns the runtime failure into a loud, early one.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Services</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> Dictionary&lt;Type, <span style="color:#66d9ef">object</span>&gt; registry = <span style="color:#66d9ef">new</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">void</span> Register&lt;T&gt;(T service)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        registry[<span style="color:#66d9ef">typeof</span>(T)] = service ?? <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ArgumentNullException(nameof(service));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> T Get&lt;T&gt;() =&gt;
</span></span><span style="display:flex;"><span>        registry.TryGetValue(<span style="color:#66d9ef">typeof</span>(T), <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> service)
</span></span><span style="display:flex;"><span>            ? (T)service
</span></span><span style="display:flex;"><span>            : <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">$&#34;No service registered for {typeof(T).Name}.&#34;</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Three decisions here do most of the heavy lifting:</p>
<ol>
<li><strong>Registration is explicit.</strong> Nothing is registered by accident or lazily. A service either exists or it does not.</li>
<li><strong>Missing services throw.</strong> There is no <code>null</code> to forget to check. The error names the missing type.</li>
<li><strong><code>null</code> is rejected at the door.</strong> You cannot register a <code>null</code> service and discover it later.</li>
</ol>
<p>The remaining rule is ordering: register before you resolve. That means a bootstrap object that runs before gameplay and populates the registry once.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">GameBootstrap</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> Awake()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Services.Register&lt;IAudioService&gt;(<span style="color:#66d9ef">new</span> UnityAudioService());
</span></span><span style="display:flex;"><span>        Services.Register&lt;ISaveService&gt;(<span style="color:#66d9ef">new</span> JsonSaveService());
</span></span><span style="display:flex;"><span>        Services.Register&lt;IInputService&gt;(<span style="color:#66d9ef">new</span> InputSystemService());
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>If the bootstrap lives in the first loaded scene and every other system is built after it, the ordering is guaranteed by the scene structure rather than by discipline.</p>
<h2 id="ambient-context-the-quieter-sibling">Ambient context, the quieter sibling</h2>
<p>The term &ldquo;ambient context&rdquo; describes a service locator with a narrower job: it exposes a small set of genuinely global values through a static entry point, and nothing else.</p>
<p>The practical difference is scope. A service locator tends to grow into a junk drawer that holds everything. An ambient context holds a handful of things that are legitimately everywhere: time, settings, a small set of core services.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">GameContext</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> AmbientServices services = <span style="color:#66d9ef">new</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> TimeProvider Time =&gt; services.Time;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> Settings Current =&gt; services.Settings;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">void</span> Initialize(TimeProvider time, Settings settings)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        services.Time = time;
</span></span><span style="display:flex;"><span>        services.Current = settings;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is still a locator under the hood. The benefit is restraint: the API surface is deliberately small, and it is obvious at a glance what is ambient and what is not.</p>
<p>The ambient context does not remove the criticisms. It just makes the surface smaller, which makes the hidden coupling easier to audit. That is a real improvement, even if it is a modest one.</p>
<h2 id="scene-scope-and-teardown">Scene scope and teardown</h2>
<p>A static registry that lives for the whole process has a second problem beyond hidden dependencies: it leaks services from unloaded scenes.</p>
<p>An audio mixer can live forever. A level-specific enemy spawner should die with the scene that created it. If the player unloads a dungeon and the registry still holds the dungeon&rsquo;s services, the next scene inherits stale state it never asked for.</p>
<p>The fix is scope, not globalism. Two levels cover most cases:</p>
<ul>
<li><strong>Project-wide</strong> for services that genuinely last the whole game: audio, settings, input, save.</li>
<li><strong>Scene-scoped</strong> for services that belong to one scene or level, created on load and discarded on unload.</li>
</ul>
<p>A scene-scoped locator does not have to be a static class. It can be a plain object owned by the scene:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SceneServices</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> Dictionary&lt;Type, <span style="color:#66d9ef">object</span>&gt; registry = <span style="color:#66d9ef">new</span>();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IServiceProvider project;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> SceneServices(IServiceProvider project)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.project = project;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Register&lt;T&gt;(T service) =&gt; registry[<span style="color:#66d9ef">typeof</span>(T)] = service!;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> T Get&lt;T&gt;() =&gt;
</span></span><span style="display:flex;"><span>        registry.TryGetValue(<span style="color:#66d9ef">typeof</span>(T), <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> service)
</span></span><span style="display:flex;"><span>            ? (T)service
</span></span><span style="display:flex;"><span>            : project.GetService(<span style="color:#66d9ef">typeof</span>(T)) <span style="color:#66d9ef">is</span> T fallback
</span></span><span style="display:flex;"><span>                ? fallback
</span></span><span style="display:flex;"><span>                : <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(<span style="color:#e6db74">$&#34;No service for {typeof(T).Name}&#34;</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The scene locator resolves its own services first and falls back to the project services for the long-lived ones. That gives you the convenience of lookup without the lifetime problem of a single process-wide bag.</p>
<p>This matters more than it sounds, because lifetime bugs in games are the ones that hide until a player does something unexpected: reloads a level, backs out to the menu, reconnects to a match. A registry that never forgets turns those into stale-state bugs that are hard to reproduce and harder to explain.</p>
<h2 id="where-the-boundary-should-sit">Where the boundary should sit</h2>
<p>The workable rule for a Unity project is a boundary, not a ban.</p>
<p><strong>Use service location:</strong></p>
<ul>
<li>inside <code>MonoBehaviour</code> components the engine constructs,</li>
<li>for ambient services like time, settings, and input,</li>
<li>in prototypes and small games where a full container is overhead,</li>
<li>at the edge, to hand off to plain C# systems.</li>
</ul>
<p><strong>Use dependency injection:</strong></p>
<ul>
<li>for plain C# classes that hold the actual logic,</li>
<li>for stable collaborators like save, audio, and repositories,</li>
<li>whenever testability of the core logic matters,</li>
<li>when the object graph is large enough that central wiring earns its keep.</li>
</ul>
<p><strong>Use neither:</strong></p>
<ul>
<li>for local state that one object naturally owns,</li>
<li>for trivial values that should just be method parameters,</li>
<li>for a class with no meaningful dependencies at all.</li>
</ul>
<p>The mistake is not using one or the other. The mistake is using both in the same layer for the same kind of dependency, so nobody can tell which rule applies where.</p>
<h2 id="the-real-cost-restated">The real cost, restated</h2>
<p>If you take one thing away, take this: the problem was never the global itself.</p>
<p>A global variable is not automatically harmful. What is harmful is a dependency that is invisible, mutable, and order-dependent. A service locator makes dependencies invisible by default. That is the entire criticism, and it is enough.</p>
<p>Constructor injection makes dependencies visible. That is the entire defense, and it is enough too.</p>
<p>So the choice is not between &ldquo;clean&rdquo; and &ldquo;dirty.&rdquo; It is between two ways of trading visibility for convenience. Service location buys convenience with visibility. That is a fair price in some places and a bad one in others.</p>
<p>The skill is knowing which place you are in.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<p><strong>Resolving in the middle of gameplay logic.</strong> A lookup buried inside a method is the hardest kind to audit. If the component pulls at the boundary in <code>Awake</code> and then treats the reference as a normal field, the damage is contained.</p>
<p><strong>Returning null instead of throwing.</strong> A <code>null</code> return spreads the failure to unrelated code. Throw at the <code>Get</code> call and name the missing type.</p>
<p><strong>Registering lazily, on first use.</strong> Lazy registration moves the &ldquo;who sets this up&rdquo; decision into whatever happens to run first. That turns initialization order into a source of bugs.</p>
<p><strong>Storing request data in the locator.</strong> A current enemy position, a damage amount, a selected target: these are not services, they are request data. Putting them in a registry is how a locator becomes a global junk drawer.</p>
<p><strong>Testing against the real registry.</strong> Tests that depend on a shared static registry will eventually interfere with each other. If you must test locator-bound code, reset the registry between tests and register fakes explicitly.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Treat the service locator as a boundary tool, not a foundation.</p>
<p>Use it where the engine or the environment has already forced a lookup: inside <code>MonoBehaviour</code> components, for ambient services, in prototypes and small games. Make it fail fast, register up front, and keep the lookup out of the core logic.</p>
<p>Use constructor injection for the plain C# classes that do the real work. Keep their dependencies visible, so the code that matters most is also the code that is easiest to read and test.</p>
<p>The pattern is not a sin. It is a trade. The only real mistake is making that trade without knowing you made it.</p>
<h2 id="references">References</h2>
<ul>
<li>Robert Nystrom, <a href="https://gameprogrammingpatterns.com/service-locator.html">Service Locator</a></li>
<li>Mark Seemann, <a href="https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/">Service Locator is an Anti-Pattern</a></li>
<li>Martin Fowler, <a href="https://martinfowler.com/articles/injection.html">Inversion of Control Containers and the Dependency Injection pattern</a></li>
<li>Unity Manual, <a href="https://docs.unity3d.com/6000.4/Documentation/Manual/class-MonoBehaviour.html">MonoBehaviour</a></li>
<li>Coyote Time, <a href="/posts/dependency-injection-in-unity/">Dependency Injection in Unity</a></li>
<li>Coyote Time, <a href="/posts/build-a-simple-di-container-in-csharp/">Build a Simple DI Container in C#</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Using Jev for Game AI: Typed Decisions for GOAP</title>
            <link>https://blog.jonmikeys.com/posts/using-jev-for-game-ai-typed-decisions-for-goap/</link>
            <pubDate>Tue, 29 Sep 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/using-jev-for-game-ai-typed-decisions-for-goap/</guid>
            <description>A practical guide to Jev&amp;rsquo;s decision types, the OpenRouter Decisions API, and using a typed choice to select an eligible GOAP goal.</description>
            <content type="html"><![CDATA[<p>An AI feature often needs a decision, not a paragraph. A support tool might need to route a ticket to one team. A game agent might need to choose one goal from a small list. Asking a chat model for prose and then parsing that prose adds work at exactly the point where code needs a predictable value.</p>
<p>Jev is built for that decision-shaped part of an application. It accepts state and typed questions, then returns answers in a known shape with probabilities. The application still owns the rules, validation, and actions that follow. That split makes Jev worth considering for classification and routing, including a narrow role in GOAP goal selection.</p>
<h2 id="what-is-jev">What is Jev?</h2>
<p>Jev is a structured decision model from TypeSafe, available through OpenRouter. It belongs to TypeSafe’s System One model family, which is designed for fast decisions inside software. Jev does not write explanations or free-form responses. Instead, the caller supplies a state object and asks one or more typed questions about it.</p>
<p>A state can be a string, an object, or an array of text data. The questions use a small set of primitives: <code>choice</code>, <code>noul</code>, and <code>score</code>. Each primitive declares the kind of answer the caller expects. Jev returns the selected value or probability, rather than asking your program to extract a label from a sentence.</p>
<p>That changes the integration boundary. A chat completion might return “The agent should rest because its fatigue is high,” and code then has to parse “rest” while deciding what to do with the explanation. Jev can instead answer a <code>choice</code> question with a stable value such as <code>FindRest</code>, plus probabilities for the options. The application validates that value and decides whether it is safe to act on.</p>
<p>Jev does not return its reasoning trace. The probabilities help express uncertainty, but they are still model output, not a proof that a decision is correct. If a feature needs a human-readable explanation, store the relevant state and decision data yourself or ask a separate generative model to explain an already validated result.</p>
<h2 id="what-is-a-classifier">What is a classifier?</h2>
<p>A classifier assigns an input to one or more predefined labels. Given a message, a classifier might choose <code>billing</code>, <code>technical</code>, or <code>sales</code>. Given an agent snapshot, it might choose <code>rest</code>, <code>fulfill-needs</code>, or <code>stay-with-group</code>.</p>
<p>A conventional classifier usually exposes a fixed input/output contract: supply an input, receive a label and often a confidence score. Jev can implement a similar decision, but its questions make the contract explicit in the request. The application says what it is deciding, describes the possible answers, and sends the state those answers should be based on.</p>
<p>That distinction is useful when the labels belong to the application. A Jev <code>choice</code> answer can be a stable ID from a game’s goal registry, an enum-like route key, or a moderation category. The model chooses among the options the caller gives it. The caller still owns the catalogue and what each value means.</p>
<p>Classification can also be single-label or multi-label. If exactly one category should apply, use a <code>choice</code> question. If several independent tags may apply at once, ask a separate yes/no <code>noul</code> question for each tag and apply thresholds in code. Avoid forcing overlapping labels into one <code>choice</code>; Jev must return one of the alternatives, even when more than one label fits.</p>
<h2 id="calling-jev-through-openrouter">Calling Jev through OpenRouter</h2>
<p>OpenRouter exposes Jev through the Decisions API at <code>https://openrouter.ai/api/alpha/decisions</code>. This is a different request shape from a chat-completions call: send a <code>model</code>, a <code>state</code>, and a <code>questions</code> object. The model ID <code>typesafe/jev-1.13</code> pins a specific Jev version. OpenRouter also documents a latest-version alias, but a pinned ID is easier to evaluate when the application depends on stable decisions.</p>
<p>The API key belongs on a trusted server. A key embedded in a Unity build can be extracted from the client, so a shipped game should send a compact decision request to its own backend. That backend can authenticate the player, apply rate limits, call OpenRouter, validate the result, and return only the permitted decision to the client. During local experiments, keep the key outside source control and out of player-facing builds.</p>
<p>Here is a small request using the <code>choice</code> primitive:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl https://openrouter.ai/api/alpha/decisions <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -H <span style="color:#e6db74">&#34;Authorization: Bearer </span>$OPENROUTER_API_KEY<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -H <span style="color:#e6db74">&#34;Content-Type: application/json&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -d <span style="color:#e6db74">&#39;{
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;model&#34;: &#34;typesafe/jev-1.13&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;state&#34;: {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      &#34;ticket&#34;: &#34;The checkout screen stays blank after I select Pay.&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      &#34;customerTier&#34;: &#34;enterprise&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    },
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;questions&#34;: {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      &#34;owner&#34;: {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &#34;type&#34;: &#34;choice&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &#34;instructions&#34;: &#34;Which team should handle this ticket?&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &#34;criteria&#34;: {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          &#34;billing&#34;: &#34;Payment processing, invoices, or refunds.&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          &#34;frontend&#34;: &#34;Rendering, layout, or browser compatibility.&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          &#34;account&#34;: &#34;Login, permissions, or profile settings.&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  }&#39;</span>
</span></span></code></pre></div><p>The response places typed answers under <code>answers</code>. A <code>choice</code> answer includes the selected key and probabilities for the listed options; a confidence value may also be present. The response’s <code>usage</code> reports request cost in <code>usage.cost</code>. Use that value for accounting instead of estimating each call from prompt length.</p>
<p>A .NET backend can send the same body with <code>HttpClient</code>. This client reads the key from the server environment, posts JSON, and returns the parsed response:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Net.Http;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Net.Http.Headers;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Text;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Text.Json;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Threading;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Threading.Tasks;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">JevClient</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> HttpClient Http = <span style="color:#66d9ef">new</span> HttpClient();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">async</span> Task&lt;JsonDocument&gt; DecideAsync(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">object</span> state,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">object</span> questions,
</span></span><span style="display:flex;"><span>        CancellationToken cancellationToken)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">string</span> apiKey = Environment.GetEnvironmentVariable(<span style="color:#e6db74">&#34;OPENROUTER_API_KEY&#34;</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">string</span>.IsNullOrWhiteSpace(apiKey))
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(<span style="color:#e6db74">&#34;OPENROUTER_API_KEY is not configured.&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> body = <span style="color:#66d9ef">new</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            model = <span style="color:#e6db74">&#34;typesafe/jev-1.13&#34;</span>,
</span></span><span style="display:flex;"><span>            state,
</span></span><span style="display:flex;"><span>            questions
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">using</span> var request = <span style="color:#66d9ef">new</span> HttpRequestMessage(
</span></span><span style="display:flex;"><span>            HttpMethod.Post,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;https://openrouter.ai/api/alpha/decisions&#34;</span>);
</span></span><span style="display:flex;"><span>        request.Headers.Authorization =
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">new</span> AuthenticationHeaderValue(<span style="color:#e6db74">&#34;Bearer&#34;</span>, apiKey);
</span></span><span style="display:flex;"><span>        request.Content = <span style="color:#66d9ef">new</span> StringContent(
</span></span><span style="display:flex;"><span>            JsonSerializer.Serialize(body),
</span></span><span style="display:flex;"><span>            Encoding.UTF8,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;application/json&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">using</span> HttpResponseMessage response = <span style="color:#66d9ef">await</span> Http.SendAsync(
</span></span><span style="display:flex;"><span>            request,
</span></span><span style="display:flex;"><span>            cancellationToken);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">string</span> responseBody = <span style="color:#66d9ef">await</span> response.Content.ReadAsStringAsync();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (!response.IsSuccessStatusCode)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> HttpRequestException(
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">$&#34;OpenRouter returned HTTP {(int)response.StatusCode}.&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> JsonDocument.Parse(responseBody);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Dispose the returned <code>JsonDocument</code> after reading it. In the GOAP example below, the <code>state</code> and <code>questions</code> arguments are the agent snapshot and candidate-goal criteria. Do not send the request to <code>/api/v1/chat/completions</code> and expect Jev’s typed response there; that endpoint is for chat-style models.</p>
<p>Jev answers the questions in one request independently. A second question cannot inspect the first question’s answer. This makes one request useful for several labels about the same state, but it does not turn the questions into a multi-step chain. If the second decision depends on the first, make that dependency explicit in your own code or send a follow-up request with the updated state.</p>
<p>At the time of writing, OpenRouter’s Jev 1.13 page lists a price of $0.042 per million input tokens and no output-token charge. Check the model page before budgeting because rates can change. Jev’s documented context window is 32,000 tokens, but a small state is still preferable: it costs less, reduces irrelevant context, and makes the decision easier to inspect.</p>
<h2 id="jevs-data-types-and-when-to-use-them">Jev’s data types and when to use them</h2>
<p>The three primitives describe different decisions. Pick the one that matches the shape of the answer instead of asking the model for a broad judgment and trying to retrofit the result later.</p>
<h3 id="choice-select-one-option"><code>choice</code>: select one option</h3>
<p>Use <code>choice</code> when exactly one option should win. The criteria object maps each stable key to a description of when that option applies. Jev returns one key and a probability for each option.</p>
<p>Examples include selecting a support queue, a permitted operation, or one eligible GOAP goal. Add an <code>other</code> or <code>none</code> option when the candidate list may not contain a valid answer. If the application must never accept an unknown value, validate the returned key against its own allowlist anyway.</p>
<h3 id="noul-estimate-whether-a-condition-is-true"><code>noul</code>: estimate whether a condition is true</h3>
<p>Use <code>noul</code> for a yes/no question. The response contains a probability that the answer is yes. The criteria describe concrete true and false cases, which is more useful than an ambiguous prompt such as “Is this important?”</p>
<p>Examples include asking whether a report describes a defect, whether a message requests a refund, or whether a character’s state suggests an eligible goal. <code>noul</code> questions are independent, so they work for multi-label tagging: ask whether each candidate tag applies, then choose the thresholds that match the product’s tolerance for false positives and false negatives.</p>
<h3 id="score-place-something-on-an-ordered-scale"><code>score</code>: place something on an ordered scale</h3>
<p>Use <code>score</code> when the answer has a real order, such as low, medium, and high urgency. Its criteria are an ordered list from the lowest level to the highest. Jev returns a probability-weighted score and probabilities for the levels; a confidence value may also be present.</p>
<p>A score is not a category with an arbitrary numeric label. The order needs to mean something. “Routine, should be handled today, blocks the simulation” has a progression; “urgent, technical, refund” does not. For a small fixed set of mutually exclusive choices, <code>choice</code> is clearer.</p>
<h2 id="using-jev-to-select-a-goap-goal">Using Jev to select a GOAP goal</h2>
<p>GOAP separates a desired world state from the actions that can make it true. A planner receives a goal and the current world state, then searches the authored action set for a valid plan. Jev can help choose which goal deserves attention when several are eligible. It should not invent actions or replace the planner’s preconditions, effects, and target checks.</p>
<p>A useful boundary looks like this:</p>
<p><img src="/images/posts/using-jev-for-game-ai-typed-decisions-for-goap/diagram.svg" alt="Jev at the GOAP boundary"></p>
<p>A crowd simulation already has structured state that can support this request: agent role, current goal, utility urgency, fulfilled and unfulfilled motivations, relationships, recent events, and the registered goals available to that role. Keep the snapshot compact and send only fields that can change the decision. The goal catalogue should remain authored data with stable wire IDs and descriptions.</p>
<p>For example, a civilian may have high fatigue, an unfulfilled motivation, and a current <code>Idle</code> goal. Local code can first remove goals that are unavailable for that agent role or invalid in the current world. The remaining candidates might be <code>FindRest</code> and <code>FulfillNeeds</code>. A <code>choice</code> request can ask Jev which goal best fits the current snapshot:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;model&#34;</span>: <span style="color:#e6db74">&#34;typesafe/jev-1.13&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;state&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;agent&#34;</span>: {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#e6db74">&#34;agent-42&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;decisionRevision&#34;</span>: <span style="color:#ae81ff">18</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;Civilian&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;currentGoal&#34;</span>: <span style="color:#e6db74">&#34;Idle&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;fatigueUrgency&#34;</span>: <span style="color:#e6db74">&#34;Critical&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;unfulfilledMotivations&#34;</span>: [<span style="color:#e6db74">&#34;Rest&#34;</span>, <span style="color:#e6db74">&#34;FindFood&#34;</span>]
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;scenario&#34;</span>: {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;danger&#34;</span>: <span style="color:#e6db74">&#34;Low&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;groupStatus&#34;</span>: <span style="color:#e6db74">&#34;Safe&#34;</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;eligibleGoals&#34;</span>: [<span style="color:#e6db74">&#34;FindRest&#34;</span>, <span style="color:#e6db74">&#34;FulfillNeeds&#34;</span>]
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;questions&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;nextGoal&#34;</span>: {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;choice&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;instructions&#34;</span>: <span style="color:#e6db74">&#34;Which eligible goal should this agent pursue next?&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;criteria&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;FindRest&#34;</span>: <span style="color:#e6db74">&#34;Reduce critical fatigue by reaching a rest spot.&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;FulfillNeeds&#34;</span>: <span style="color:#e6db74">&#34;Address an unfulfilled motivation when immediate rest is not the stronger need.&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The descriptions are part of the decision contract. “Find rest” is too vague if the agent needs a particular condition or role. “Reduce critical fatigue by reaching a rest spot” tells Jev what success means without asking it to plan the movement or decide whether a rest spot exists. The code should derive <code>eligibleGoals</code> from the registry and current state, not trust a model to discover which goal classes exist.</p>
<p>The response’s <code>nextGoal.choice</code> can then be mapped through the project’s stable GOAP wire-ID registry. Before requesting a plan, the caller should check all of the following:</p>
<ul>
<li>The key parses to a registered goal ID.</li>
<li>The goal is in the allowlist for this agent and remains eligible.</li>
<li>The agent and scenario versions still match the snapshot used for the request.</li>
<li>The answer has enough confidence for the application’s chosen policy.</li>
</ul>
<p>A small adapter can enforce those checks before handing the goal to the existing GOAP provider. This example fails closed when confidence is missing; its caller can keep the behavior graph’s current decision when it returns <code>false</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Collections.Generic;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> Newtonsoft.Json;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> Newtonsoft.Json.Linq;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">static</span> <span style="color:#66d9ef">bool</span> TryApplyChoice(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">string</span> responseJson,
</span></span><span style="display:flex;"><span>    AgentCortex cortex,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> requestedDecisionRevision,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> currentDecisionRevision,
</span></span><span style="display:flex;"><span>    ISet&lt;GoapGoalId&gt; currentlyEligibleGoals,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">double</span> minimumConfidence)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">string</span>.IsNullOrWhiteSpace(responseJson) || cortex == <span style="color:#66d9ef">null</span> ||
</span></span><span style="display:flex;"><span>        requestedDecisionRevision != currentDecisionRevision ||
</span></span><span style="display:flex;"><span>        currentlyEligibleGoals == <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    JObject root;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        root = JObject.Parse(responseJson);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">catch</span> (JsonException)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    JToken answer = root[<span style="color:#e6db74">&#34;answers&#34;</span>]?[<span style="color:#e6db74">&#34;nextGoal&#34;</span>];
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (answer == <span style="color:#66d9ef">null</span> || answer[<span style="color:#e6db74">&#34;choice&#34;</span>]?.Type != JTokenType.String)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    JToken confidenceToken = answer[<span style="color:#e6db74">&#34;confidence&#34;</span>];
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (confidenceToken == <span style="color:#66d9ef">null</span> ||
</span></span><span style="display:flex;"><span>        (confidenceToken.Type != JTokenType.Float &amp;&amp;
</span></span><span style="display:flex;"><span>         confidenceToken.Type != JTokenType.Integer))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">double</span> confidence = confidenceToken.Value&lt;<span style="color:#66d9ef">double</span>&gt;();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (confidence &lt; <span style="color:#ae81ff">0</span> || confidence &gt; <span style="color:#ae81ff">1</span> || confidence &lt; minimumConfidence)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">string</span> wireId = (<span style="color:#66d9ef">string</span>)answer[<span style="color:#e6db74">&#34;choice&#34;</span>];
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (!GoapGoalRegistry.TryParseWireId(wireId, <span style="color:#66d9ef">out</span> GoapGoalId goalId) ||
</span></span><span style="display:flex;"><span>        !currentlyEligibleGoals.Contains(goalId))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> cortex.TryExecuteAvailableGoals(<span style="color:#66d9ef">new</span>[] { goalId });
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The example’s <code>currentlyEligibleGoals</code> set should be rebuilt from the current role’s registered goals after deterministic state checks. <code>requestedDecisionRevision</code> must cover every agent and scenario field sent to Jev; increment it whenever any of those inputs changes. The reference snapshot combines <code>AgentContext</code> data with utility urgency and motivations read from other systems, so <code>AgentContext.StateVersion</code> alone may miss a fatigue or motivation change. <code>TryExecuteAvailableGoals</code> adds one final local validation before requesting the goal.</p>
<p>When the response arrives, reject it if the decision revision changed or the selected goal is no longer eligible. If the JSON is malformed or confidence is below the application’s threshold, keep the existing behavior-graph decision or use a deterministic local fallback. Choose that threshold from evaluation data rather than intuition alone.</p>
<p>Once accepted, pass the typed goal to the GOAP provider. The planner still checks its own conditions, sensors, targets, costs, and effects. For <code>FindRest</code>, for example, the goal condition is tied to critical fatigue, and the rest capability has a target sensor for a nearby rest spot. If the sensor finds no usable target or the action becomes invalid, normal GOAP validation and replanning remain responsible for recovery. Jev selected intent; the planner still decides whether the authored plan can run.</p>
<p>The example borrows a useful boundary from a crowd-simulation codebase: build an agent snapshot, limit the candidate list to registered goals, and validate returned IDs and versions. That project’s documentation describes behavior-graph nodes selecting goals in the live runtime, with an LLM interpreting spawn requests. The Jev flow here is a proposed decision provider at the goal-selection boundary, not a claim that Jev already drives that simulation.</p>
<h2 id="keep-the-model-at-the-decision-boundary">Keep the model at the decision boundary</h2>
<p>Calling a remote model for every agent on every frame would make latency, cost, and stale responses part of the simulation loop. Schedule decisions when meaningful state changes, batch requests where the API and game design allow it, and keep a deterministic behavior available while a request is in flight. Log the snapshot version, model ID, selected key, probabilities, latency, and <code>usage.cost</code> so the decision can be reviewed later.</p>
<p>Start with one decision boundary and a small candidate set. Log how often Jev’s selection passes validation, how often the local fallback takes over, and whether the chosen goals lead to valid plans. If those measurements do not show a useful change in behavior, keep the existing behavior-graph decision.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://openrouter.ai/docs/guides/community/jev">Jev on OpenRouter</a></li>
<li><a href="https://openrouter.ai/docs/guides/community/jev-tutorial">OpenRouter Jev tutorial</a></li>
<li><a href="https://openrouter.ai/docs/api/api-reference/alphadecisions/submit-a-decisions-questions-and-answers-request">Decisions API request reference</a></li>
<li><a href="https://openrouter.ai/typesafe/jev-1.13">Jev 1.13 model and pricing</a></li>
<li><a href="/posts/goap-for-game-ai/">GOAP for Game AI: Let Actions Build the Plan</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Visitor Pattern in Game Code: Add Tools Around Stable Data</title>
            <link>https://blog.jonmikeys.com/posts/visitor-pattern-in-game-code/</link>
            <pubDate>Tue, 29 Sep 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/visitor-pattern-in-game-code/</guid>
            <description>A practical C# guide to the Visitor pattern in game code: where double dispatch helps, how to validate ability data, and when a switch or ordinary methods are clearer.</description>
            <content type="html"><![CDATA[<p>A game ability often starts as a few fields: a damage amount, a cooldown, and a status effect. Then designers ask for combinations. An ability can deal damage, apply a status, branch on a target condition, or run several effects in sequence. The data becomes a small tree, and tools need to inspect it in different ways.</p>
<p>A validator checks that every status id exists. A balance report totals damage and duration. An editor preview turns the tree into readable text. A build step gathers references so unused assets can be found. Putting all of those jobs on the data nodes couples the model to every tool. Copying a chain of type checks into each tool spreads the same knowledge around.</p>
<p>The Visitor pattern is one way to keep a stable set of node types separate from a growing set of operations. In game code, that can be a useful fit for content validation and editor or build tooling. It is usually a poor fit for every object in a running scene. The choice depends on which side of the model changes more often.</p>
<p><img src="/images/posts/visitor-pattern-in-game-code/visitor-flow.svg" alt="Diagram of an ability tree dispatching to type-specific visitor operations"></p>
<h2 id="the-useful-shape-of-the-problem">The useful shape of the problem</h2>
<p>Visitor works on a known family of element types. Think of a content tree with <code>DamageNode</code>, <code>ApplyStatusNode</code>, and <code>SequenceNode</code>. Those types describe the shape of an ability. They do not decide how the ability is validated, summarized, exported, or previewed.</p>
<p>That separation is useful when the node model is fairly stable but new tools keep appearing. Adding an operation means adding a visitor. The node classes stay focused on representing data and enforcing their own basic invariants. A designer-facing validator can report bad references without giving every node a dependency on the editor. A build report can inspect the same data without adding build-only methods to runtime types.</p>
<p>The trade is deliberate. When a new node type appears, every visitor must decide what that node means. A <code>ConditionalNode</code> added to the tree affects the validator, exporter, balance report, and any other complete visitor. That extra work is a safeguard when skipping the new case would make a report incomplete. It is friction when node types change every week.</p>
<h2 id="double-dispatch-without-the-fog">Double dispatch, without the fog</h2>
<p>C# normally chooses an overloaded method using the argument&rsquo;s compile-time type. If a method receives an <code>AbilityNode</code>, an overload for <code>DamageNode</code> is not selected merely because the object happens to be a damage node at runtime. Virtual dispatch chooses an implementation based on the runtime type of the receiver; it does not also dispatch on the runtime type of every argument.</p>
<p>Visitor makes two calls. First, the node&rsquo;s virtual <code>Accept</code> method selects the concrete node implementation. That method then calls the matching overload on the visitor. In a <code>DamageNode.Accept</code> method, <code>this</code> has the compile-time type <code>DamageNode</code>, so C# selects <code>Visit(DamageNode)</code>. Those two single-dispatch steps are called double dispatch.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Collections.Generic;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IAbilityVisitor</span>&lt;TResult&gt;
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    TResult Visit(DamageNode node);
</span></span><span style="display:flex;"><span>    TResult Visit(ApplyStatusNode node);
</span></span><span style="display:flex;"><span>    TResult Visit(SequenceNode node);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">AbilityNode</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">abstract</span> TResult Accept&lt;TResult&gt;(IAbilityVisitor&lt;TResult&gt; visitor);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DamageNode</span> : AbilityNode
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> Amount { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> DamageNode(<span style="color:#66d9ef">float</span> amount) =&gt; Amount = amount;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">override</span> TResult Accept&lt;TResult&gt;(IAbilityVisitor&lt;TResult&gt; visitor)
</span></span><span style="display:flex;"><span>        =&gt; visitor.Visit(<span style="color:#66d9ef">this</span>);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ApplyStatusNode</span> : AbilityNode
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">string</span> StatusId { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> Duration { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> ApplyStatusNode(<span style="color:#66d9ef">string</span> statusId, <span style="color:#66d9ef">float</span> duration)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        StatusId = statusId;
</span></span><span style="display:flex;"><span>        Duration = duration;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">override</span> TResult Accept&lt;TResult&gt;(IAbilityVisitor&lt;TResult&gt; visitor)
</span></span><span style="display:flex;"><span>        =&gt; visitor.Visit(<span style="color:#66d9ef">this</span>);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SequenceNode</span> : AbilityNode
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IReadOnlyList&lt;AbilityNode&gt; Children { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> SequenceNode(IEnumerable&lt;AbilityNode&gt; children)
</span></span><span style="display:flex;"><span>        =&gt; Children = Array.AsReadOnly(<span style="color:#66d9ef">new</span> List&lt;AbilityNode&gt;(children).ToArray());
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">override</span> TResult Accept&lt;TResult&gt;(IAbilityVisitor&lt;TResult&gt; visitor)
</span></span><span style="display:flex;"><span>        =&gt; visitor.Visit(<span style="color:#66d9ef">this</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Each concrete node implements one overload from <code>IAbilityVisitor&lt;TResult&gt;</code>. The visitor&rsquo;s result type is generic so one operation can return diagnostics, another can return a formatted description, and a third can return a number without stashing results in mutable fields.</p>
<p>For a real content model, the node types also need their data. A status node might expose a status id and duration. A sequence node holds an ordered list of child nodes. The sequence owns that list because ordering is part of the ability&rsquo;s meaning; visitors choose how to process it. Keeping this model as ordinary C# data also makes it possible to test without loading a scene or constructing <code>MonoBehaviour</code> instances.</p>
<h2 id="a-visitor-that-validates-ability-content">A visitor that validates ability content</h2>
<p>Validation is a good first operation because its rules are easy to state and its output is useful before a match starts. Damage must be finite and positive. A status needs a non-empty id and a finite, positive duration. A sequence is valid only when its children are valid.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Collections.Generic;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">AbilityValidator</span> : IAbilityVisitor&lt;IReadOnlyList&lt;<span style="color:#66d9ef">string</span>&gt;&gt;
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IReadOnlyList&lt;<span style="color:#66d9ef">string</span>&gt; Visit(DamageNode node)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">float</span>.IsNaN(node.Amount) || <span style="color:#66d9ef">float</span>.IsInfinity(node.Amount) || node.Amount &lt;= <span style="color:#ae81ff">0f</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span>[] { <span style="color:#e6db74">&#34;Damage must be finite and greater than zero.&#34;</span> };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> Array.Empty&lt;<span style="color:#66d9ef">string</span>&gt;();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IReadOnlyList&lt;<span style="color:#66d9ef">string</span>&gt; Visit(ApplyStatusNode node)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> errors = <span style="color:#66d9ef">new</span> List&lt;<span style="color:#66d9ef">string</span>&gt;();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">string</span>.IsNullOrWhiteSpace(node.StatusId))
</span></span><span style="display:flex;"><span>            errors.Add(<span style="color:#e6db74">&#34;Status id is required.&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">float</span>.IsNaN(node.Duration) || <span style="color:#66d9ef">float</span>.IsInfinity(node.Duration) || node.Duration &lt;= <span style="color:#ae81ff">0f</span>)
</span></span><span style="display:flex;"><span>            errors.Add(<span style="color:#e6db74">&#34;Status duration must be finite and greater than zero.&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> errors;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IReadOnlyList&lt;<span style="color:#66d9ef">string</span>&gt; Visit(SequenceNode node)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> errors = <span style="color:#66d9ef">new</span> List&lt;<span style="color:#66d9ef">string</span>&gt;();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">foreach</span> (<span style="color:#66d9ef">var</span> child <span style="color:#66d9ef">in</span> node.Children)
</span></span><span style="display:flex;"><span>            errors.AddRange(child.Accept(<span style="color:#66d9ef">this</span>));
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> errors;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The sequence visitor owns the recursive step because it knows that this node contains children. The leaf visitors validate only their own fields. A caller can validate a root with <code>root.Accept(new AbilityValidator())</code> and show the returned messages in an inspector, content pipeline, or command-line check. A small edit-mode test can protect the traversal contract:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> ability = <span style="color:#66d9ef">new</span> SequenceNode(<span style="color:#66d9ef">new</span> AbilityNode[]
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">new</span> DamageNode(<span style="color:#ae81ff">12f</span>),
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">new</span> ApplyStatusNode(<span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#ae81ff">3f</span>)
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> errors = ability.Accept(<span style="color:#66d9ef">new</span> AbilityValidator());
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (errors.Count != <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> Exception(<span style="color:#e6db74">$&#34;Expected one validation error, got {errors.Count}.&#34;</span>);
</span></span></code></pre></div><p>This checks that the sequence visits both children and that invalid status data produces a diagnostic. A larger test suite can assert the exact message and include nested sequences, but this small case catches a missing recursive step.</p>
<p>This example returns errors rather than keeping a list on the visitor instance. That makes each call&rsquo;s output explicit and avoids stale diagnostics when someone reuses the same visitor for a second asset. For a very large tree, repeated lists and <code>AddRange</code> may allocate more than desired. Measure the editor or build step before changing the API; if it matters, pass a caller-owned result buffer or use a visitor with carefully scoped state.</p>
<h2 id="the-model-has-a-cost">The model has a cost</h2>
<p>The visitor interface lists every concrete element type. That list is the contract for the model. When a new <code>HealNode</code> is introduced, each concrete <code>IAbilityVisitor&lt;TResult&gt;</code> implementation must add <code>Visit(HealNode)</code>. The compiler points to implementations that have not been updated.</p>
<p>That pressure is valuable for exhaustive operations. A validator that forgets a new node should not report success by accident. A serializer or balance exporter should make an explicit decision about how healing behaves. If an operation truly does not care about a node, a default visitor can say so, but silent no-op defaults trade away the compiler&rsquo;s reminder. Use them only for intentionally partial operations, such as a visitor that searches for one optional reference type.</p>
<p>The inverse cost appears when adding operations. A new visitor can be added without changing every node class. This is the pattern&rsquo;s main benefit. It moves the change cost toward extending operations and away from extending the data hierarchy.</p>
<p>That choice resembles the expression problem: a design cannot make both adding new variants and adding new operations equally cheap. Visitor favors a stable set of variants and many independent operations. If that is not how the game model evolves, the pattern is pushing against the project.</p>
<h2 id="traversal-is-a-separate-decision">Traversal is a separate decision</h2>
<p>Visitor decides which operation handles a node. It does not automatically define how a tree is walked. In the validator above, <code>Visit(SequenceNode)</code> walks the children. A text exporter may walk them in order but add indentation. A dependency collector may need to de-duplicate asset ids. A balance summary may carry totals or context from parent nodes.</p>
<p>Putting traversal in each visitor is simple while the operations genuinely need different traversal rules. It becomes repetitive when several operations must walk exactly the same structure. At that point, consider a small shared walker or a base visitor that traverses children and lets subclasses handle node-specific work. Keep that abstraction narrow. A traversal framework with configurable enter/exit hooks, mutation policies, cancellation, and scheduling is a new subsystem; do not build it for three <code>foreach</code> loops.</p>
<p>A conditional ability node makes the difference concrete. The runtime evaluator follows the branch selected by the current target or game state. A content validator should inspect both branches, including the one that is not active in the editor preview, so a missing status id does not wait until a rare condition occurs. The same node hierarchy can therefore need distinct traversal policies. Sharing dispatch is useful; forcing every operation through one traversal can hide content errors.</p>
<p>Mutation during traversal deserves a clear rule. If a visitor rewrites or removes nodes while iterating the same child collection, ordering and skipped-element behavior become accidental. Prefer visitors that return a replacement tree, or gather edits and apply them after the walk. A reporting visitor should not quietly mutate the data it is reporting on.</p>
<h2 id="a-switch-can-be-the-better-c-answer">A switch can be the better C# answer</h2>
<p>A local switch expression often keeps a small external operation shorter than classic Visitor. Keep leaf validation in small helper methods, then make the dispatch explicit:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Collections.Generic;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Linq;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">static</span> IReadOnlyList&lt;<span style="color:#66d9ef">string</span>&gt; Validate(AbilityNode node) =&gt; node <span style="color:#66d9ef">switch</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    DamageNode damage =&gt; ValidateDamage(damage),
</span></span><span style="display:flex;"><span>    ApplyStatusNode status =&gt; ValidateStatus(status),
</span></span><span style="display:flex;"><span>    SequenceNode sequence =&gt; sequence.Children
</span></span><span style="display:flex;"><span>        .SelectMany(Validate)
</span></span><span style="display:flex;"><span>        .ToArray(),
</span></span><span style="display:flex;"><span>    _ =&gt; <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> NotSupportedException(<span style="color:#e6db74">$&#34;Unhandled node type: {node.GetType().Name}&#34;</span>)
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>The fallback makes an unknown node fail instead of quietly disappearing. With an open abstract base class, the compiler cannot always prove that the switch covers every subtype. Newer C# versions and analyzers can improve diagnostics for closed type hierarchies, but check the language version and compiler settings actually used by the Unity project. A <code>_ =&gt; Array.Empty&lt;string&gt;()</code> fallback is shorter, but it can hide missing behavior.</p>
<p>A switch is often easier to read when there are only one or two operations. It also keeps the data classes unaware of a visitor interface. Classic Visitor becomes more attractive when several operations need their own configuration, result type, lifecycle, or polymorphic selection, or when an existing framework already exposes a visitor API.</p>
<p>Do not create both a visitor interface and a large set of switches for the same hierarchy without a reason. That leaves two dispatch systems to update. Pick the one that matches the model and the language support in the project.</p>
<h2 id="where-it-fits-in-unity">Where it fits in Unity</h2>
<p>A Unity project can keep the visited model separate from scene objects. A <code>ScriptableObject</code> may hold authoring data; an import or validation step can convert that data to a plain node graph; visitors can inspect the graph before it produces runtime objects. The exact serialization design is a separate choice. Unity&rsquo;s serialization support for custom polymorphic data has constraints, so confirm how the project stores the hierarchy rather than assuming that an interface-shaped object graph will serialize automatically.</p>
<p>Unity also provides a Properties API with property bags and property visitors for inspecting .NET objects. That is a better starting point when the job is generic property traversal, such as building inspector-like tooling or walking reflected data. A custom ability visitor is useful when the rules are domain-specific: validate that a status id exists in this game&rsquo;s catalog, or calculate a balance report using this game&rsquo;s conventions. Reimplementing generic property walking to handle those rules adds work without adding meaning.</p>
<p>This kind of visitor is most comfortable in editor tooling, content validation, import pipelines, tests, and infrequent debug reports. It can run at runtime too, but a per-frame visitor over thousands of nodes is a design decision worth measuring. Each node visit involves virtual dispatch, and allocating a visitor or result collection on every frame can create avoidable garbage. For frequent gameplay decisions, ordinary methods, data tables, or a purpose-built evaluator may express the behavior with less machinery.</p>
<p>Avoid making <code>GameObject</code> or <code>MonoBehaviour</code> hierarchies accept a new visitor just to calculate ordinary gameplay behavior. Components already expose operations through methods and interfaces. A damage system that needs to apply damage can call an <code>IDamageable</code> contract. Keep the visitor for external passes over a stable structure; use ordinary polymorphic methods for one game object at a time.</p>
<h2 id="visitor-versus-strategy">Visitor versus Strategy</h2>
<p>These two patterns can look similar because both move behavior out of a class. Strategy swaps one algorithm behind a stable interface. A movement controller can choose a ground or flying movement strategy. The context asks one strategy to perform one job.</p>
<p>Visitor applies an operation across several known element types. A balance visitor can visit damage, status, and sequence nodes and compute one report over the complete ability tree. Strategy varies the implementation of one behavior; Visitor varies the operation over a structure. If the code needs one interchangeable behavior at a time, start with Strategy or a normal interface. If it needs many distinct passes over a stable tree, Visitor is worth considering.</p>
<h2 id="a-practical-decision-check">A practical decision check</h2>
<p>Before adding <code>Accept</code> methods, ask what actually changes in this feature.</p>
<ul>
<li>If node types are stable and tooling operations keep growing, Visitor may centralize those operations cleanly.</li>
<li>If node types change often, prefer methods on the types, a switch with explicit failure, or a model designed for open extension.</li>
<li>If there is one small operation, write the method or switch first. A visitor interface is unlikely to repay its extra files.</li>
<li>If the job is generic reflection or property traversal, look for a platform API before writing a custom walker.</li>
<li>If the work runs in a hot gameplay loop, measure allocations and dispatch cost, then compare the visitor against a direct evaluator.</li>
</ul>
<p>For a stable ability-data tree, keep validators and reports beside the content pipeline as separate visitors. Keep per-frame combat behavior on gameplay interfaces or a purpose-built evaluator, and add the visitor boundary only when multiple tools are starting to crowd the data model.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://refactoring.guru/design-patterns/visitor">Visitor pattern and double dispatch</a></li>
<li><a href="https://refactoring.guru/design-patterns/visitor-double-dispatch">Double dispatch explained</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/patterns/pattern-matching">C# pattern matching overview</a></li>
<li><a href="https://docs.unity3d.com/2022.3/Documentation/Manual/properties.html">Unity Properties and property visitors</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Extension Methods in Unity: Make Calls Clear, Not Clever</title>
            <link>https://blog.jonmikeys.com/posts/extension-methods-in-unity/</link>
            <pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/extension-methods-in-unity/</guid>
            <description>Use extension methods as small, honest vocabulary for Unity code while avoiding invisible cost, ambiguous APIs, fake-null traps, and miscellaneous helper drawers.</description>
            <content type="html"><![CDATA[<p>Extension methods are one of C#&rsquo;s smallest features and one of Unity&rsquo;s easiest places to accumulate design debt. A method that begins as a convenient two-line helper can spread through every assembly, hide an allocation in a hot loop, collide with another package, or make engine work look like a cheap property lookup.</p>
<p>The language feature itself is straightforward. A static method whose first parameter is marked with <code>this</code> can be called as though it were an instance member of that parameter&rsquo;s type. The object is still passed to a static method; C# merely provides more convenient call syntax.</p>
<p>That distinction should guide the design. An extension method does not add state, gain privileged access, or become part of the extended class. It is a small piece of API vocabulary placed near a type. Used carefully, that vocabulary makes gameplay code read in the same order as the operation it describes. Used casually, it creates a second, less visible API for every common Unity type.</p>
<p>The useful question is therefore not “can this be an extension method?” Most static helpers can. The useful question is whether extension syntax makes the call more honest and easier to find without hiding an important dependency or cost.</p>
<h2 id="what-the-compiler-actually-does">What the compiler actually does</h2>
<p>Consider a compact transform helper:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">TransformExtensions</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">void</span> SetX(<span style="color:#66d9ef">this</span> Transform transform, <span style="color:#66d9ef">float</span> x)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Vector3 position = transform.position;
</span></span><span style="display:flex;"><span>        position.x = x;
</span></span><span style="display:flex;"><span>        transform.position = position;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Callers can write this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>playerTransform.SetX(spawnX);
</span></span></code></pre></div><p>Conceptually, the compiler binds that call to this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>TransformExtensions.SetX(playerTransform, spawnX);
</span></span></code></pre></div><p>The extension method remains static. It can use only public and otherwise accessible members. It does not modify <code>Transform</code> or participate in its virtual dispatch. The call is resolved at compile time from the receiver&rsquo;s declared type, the extension methods currently in scope, and the available overloads.</p>
<p>This explains several behaviours that otherwise look surprising.</p>
<p>An actual instance member wins over an extension method with the same applicable signature. If Unity later adds an instance method named <code>SetX</code>, existing source may bind differently after an engine upgrade. An extension declared for a derived type can be selected when the variable is declared as that derived type, but not when the same object is held in a base-typed variable. Adding a <code>using</code> directive can introduce another candidate and turn a previously clear call into an ambiguous one.</p>
<p>Extension methods are compile-time convenience, not runtime decoration. Treating them that way removes much of the mystery.</p>
<h2 id="use-extension-syntax-when-the-receiver-is-the-subject">Use extension syntax when the receiver is the subject</h2>
<p>The strongest extension methods describe an operation whose natural subject is the left-hand value. Collection operations are the familiar example: the sequence is filtered, mapped, or searched. Unity code has similar cases.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LayerMaskExtensions</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">bool</span> Contains(<span style="color:#66d9ef">this</span> LayerMask mask, <span style="color:#66d9ef">int</span> layer)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> layer &gt;= <span style="color:#ae81ff">0</span> &amp;&amp; layer &lt; <span style="color:#ae81ff">32</span> &amp;&amp; (mask.<span style="color:#66d9ef">value</span> &amp; (<span style="color:#ae81ff">1</span> &lt;&lt; layer)) != <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The call reads naturally:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (damageableLayers.Contains(other.gameObject.layer))
</span></span><span style="display:flex;"><span>    ApplyDamage(other.gameObject);
</span></span></code></pre></div><p>The mask is clearly the subject. The helper is deterministic, cheap, and needs no hidden service. Its name describes the result rather than the implementation. Those qualities make extension syntax earn its place.</p>
<p>Compare that with an operation such as this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">void</span> SaveToCloud(<span style="color:#66d9ef">this</span> PlayerState state)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Finds authentication, serialization, and network services globally.</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The sentence is fluent, but the design is not. Saving is not behavior supplied by <code>PlayerState</code>; it requires a destination, credentials, serialization policy, cancellation, retries, and error handling. Hiding those dependencies inside an extension makes the call look local and cheap when it is neither.</p>
<p>Prefer an explicit service:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">await</span> cloudSave.SaveAsync(state, cancellationToken);
</span></span></code></pre></div><p>A useful extension method is usually about the receiver. A suspicious one merely starts from the receiver and then reaches into the rest of the application.</p>
<h2 id="keep-cost-visible-in-the-name-and-shape">Keep cost visible in the name and shape</h2>
<p>A method call does not need to be free, but its cost should be unsurprising. Unity projects are especially sensitive because apparently harmless calls often involve native-engine boundaries, hierarchy walks, physics queries, asset loading, or managed allocations.</p>
<p>This helper is convenient and misleading:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> T RequireInChildren&lt;T&gt;(<span style="color:#66d9ef">this</span> Component component)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">where</span> T : Component
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    T found = component.GetComponentInChildren&lt;T&gt;(<span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (found == <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> MissingComponentException(<span style="color:#66d9ef">typeof</span>(T).Name);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> found;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>RequireInChildren</code> at least signals a hierarchy search better than a vague name such as <code>Get</code>. It still should not be called repeatedly from <code>Update</code>. The extension syntax can make the operation feel like direct access to a field on <code>component</code>, even though Unity may inspect a subtree.</p>
<p>A practical rule is to make expensive work visible through verbs and context:</p>
<ul>
<li><code>FindClosestTarget</code> implies a search; <code>ClosestTarget</code> can look like cached data.</li>
<li><code>LoadIconAsync</code> exposes both loading and asynchronous completion.</li>
<li><code>CopyComponentsTo</code> suggests work proportional to a collection.</li>
<li><code>GetOrAddComponent</code> announces that the scene may be mutated.</li>
<li><code>ToArray</code> should be assumed to allocate unless documentation proves otherwise.</li>
</ul>
<p>Do not use an extension method to disguise an operation the caller must budget. If the operation needs caching, batching, cancellation, ownership, or scheduling, a normal object with explicit state is usually a better home.</p>
<h2 id="do-not-turn-gameobject-into-a-service-locator">Do not turn <code>GameObject</code> into a service locator</h2>
<p><code>GameObject</code>, <code>Component</code>, and <code>Transform</code> are tempting receivers because almost every Unity system already has one. That makes them poor places for broad application services.</p>
<p>A helper such as <code>gameObject.PlaySound(&quot;hit&quot;)</code> might search for an audio manager, load a clip, choose a mixer group, and create a pooled source. Another helper might call <code>transform.ShowDamageNumber(20)</code>, then find UI and localization services. Soon every scene object appears able to perform every operation in the game.</p>
<p>This style reduces visible parameters but does not reduce coupling. It moves coupling into static code and global lookup. Tests need more scene setup, execution order matters, and readers cannot tell which systems a method touches from its call site.</p>
<p>Keep engine-type extensions close to engine-type concerns. Converting coordinates, checking a layer mask, iterating direct children, setting one axis, or attempting a well-defined component lookup can fit. Starting quests, saving progress, spawning networked actors, loading addressable content, and submitting telemetry usually do not.</p>
<p>The receiver should carry most of the information needed by the operation. If the method needs three managers and a hidden singleton, extension syntax is polishing the wrong design.</p>
<h2 id="unity-null-is-not-ordinary-c-null">Unity null is not ordinary C# null</h2>
<p>An extension method can be invoked with a null receiver because it is a static call underneath. Ordinary instance syntax would throw before entering a method; extension syntax passes null as the first argument.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>Transform target = <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>target.SetX(<span style="color:#ae81ff">10f</span>); <span style="color:#75715e">// Enters SetX, then fails when position is accessed.</span>
</span></span></code></pre></div><p>Unity adds another complication. <code>UnityEngine.Object</code> overloads equality and Boolean conversion so a managed wrapper can behave as null after its native object has been destroyed. Microsoft’s null operators and Unity’s overloaded null semantics are not interchangeable.</p>
<p>That means a generic-looking helper can get the check wrong:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">bool</span> IsNull&lt;T&gt;(<span style="color:#66d9ef">this</span> T <span style="color:#66d9ef">value</span>)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">value</span> <span style="color:#66d9ef">is</span> <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>For a destroyed Unity object, <code>is null</code> checks the managed reference and can report false even though Unity&rsquo;s overloaded <code>== null</code> reports true. The helper sounds universal while silently choosing one null model.</p>
<p>Do not add <code>IsNull</code> and <code>IsNotNull</code> extensions to a Unity project. They save almost no text and obscure which semantics are required. For Unity objects, use the engine&rsquo;s intended check directly:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (target == <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span>;
</span></span></code></pre></div><p>For unconstrained managed data where actual reference null is intended, use standard C# null patterns in the local code.</p>
<p>Extensions on <code>UnityEngine.Object</code> should validate the receiver at the boundary when failure would otherwise be cryptic:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> T RequireComponent&lt;T&gt;(<span style="color:#66d9ef">this</span> GameObject gameObject)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">where</span> T : Component
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (gameObject == <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> System.ArgumentNullException(nameof(gameObject));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (gameObject.TryGetComponent(<span style="color:#66d9ef">out</span> T component))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> component;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> MissingComponentException(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">$&#34;{gameObject.name} requires {typeof(T).Name}.&#34;</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is suitable for setup and authoring validation, not a per-frame optional query. Throwing is useful when the missing component violates a declared invariant. When absence is expected, Unity&rsquo;s existing <code>TryGetComponent</code> already expresses the case correctly and needs no wrapper.</p>
<h2 id="prefer-concrete-contracts-over-universal-generic-helpers">Prefer concrete contracts over universal generic helpers</h2>
<p>Generic extensions can remove repetition, but broad constraints often produce vague APIs. A method on <code>this T</code> is effectively available everywhere its namespace is imported. The wider the receiver, the more carefully the name must be chosen.</p>
<p>This extension adds little:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">bool</span> IsOneOf&lt;T&gt;(<span style="color:#66d9ef">this</span> T <span style="color:#66d9ef">value</span>, <span style="color:#66d9ef">params</span> T[] choices)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> choices.Contains(<span style="color:#66d9ef">value</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>It appears on nearly every type, imports LINQ behaviour indirectly, and allocates a <code>params</code> array at typical call sites. In a frame loop, a neat sentence can become recurring garbage. At only two or three choices, direct comparisons are both clearer and cheaper. If the set is reused or large, a cached <code>HashSet&lt;T&gt;</code> expresses the real data structure.</p>
<p>A constrained helper can be more honest:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ComponentExtensions</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">bool</span> TryGetEnabled&lt;T&gt;(<span style="color:#66d9ef">this</span> Component owner, <span style="color:#66d9ef">out</span> T component)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">where</span> T : Behaviour
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (owner != <span style="color:#66d9ef">null</span> &amp;&amp;
</span></span><span style="display:flex;"><span>            owner.TryGetComponent(<span style="color:#66d9ef">out</span> component) &amp;&amp;
</span></span><span style="display:flex;"><span>            component.enabled)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        component = <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Even here, the narrower promise matters: this operation applies only to <code>Behaviour</code> components and succeeds only when the sibling component is enabled. Inspect the intended Unity version and component-query behaviour before standardizing any lookup helper. A project-local helper should match the engine version the project actually ships, not a remembered limitation from an older release.</p>
<p>The general lesson is to constrain both the type and the promise. <code>LayerMask.Contains(int)</code> says more than <code>Object.Matches&lt;T&gt;()</code>. A narrow extension is easier to discover, harder to misuse, and less likely to collide with another package.</p>
<h2 id="avoid-hidden-allocations-in-fluent-chains">Avoid hidden allocations in fluent chains</h2>
<p>Fluent syntax encourages chaining, and chaining can conceal intermediate work. LINQ is valuable in editor tooling, setup code, and many low-frequency systems. It can also allocate enumerators, iterators, closures, and result collections depending on the operators, source type, runtime, and materialization step.</p>
<p>An extension does not need LINQ to allocate. Common traps include:</p>
<ul>
<li><code>params</code> arrays created at each call site</li>
<li>capturing lambdas that create closures</li>
<li>iterator methods using <code>yield return</code></li>
<li>string interpolation used only for disabled logging</li>
<li>returning a new list or array for every query</li>
<li>boxing a struct through an interface</li>
<li>repeated component or hierarchy searches presented as accessors</li>
</ul>
<p>Suppose a targeting helper returns a fresh list:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> List&lt;Enemy&gt; InRange(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">this</span> IReadOnlyList&lt;Enemy&gt; enemies,
</span></span><span style="display:flex;"><span>    Vector3 origin,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">float</span> radius)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> result = <span style="color:#66d9ef">new</span> List&lt;Enemy&gt;();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">float</span> radiusSquared = radius * radius;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i = <span style="color:#ae81ff">0</span>; i &lt; enemies.Count; i++)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Enemy enemy = enemies[i];
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (enemy != <span style="color:#66d9ef">null</span> &amp;&amp;
</span></span><span style="display:flex;"><span>            (enemy.transform.position - origin).sqrMagnitude &lt;= radiusSquared)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            result.Add(enemy);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The implementation avoids a square root but still allocates a list on every call. A reusable output buffer makes ownership and lifetime explicit:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">void</span> CollectInRange(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">this</span> IReadOnlyList&lt;Enemy&gt; enemies,
</span></span><span style="display:flex;"><span>    Vector3 origin,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">float</span> radius,
</span></span><span style="display:flex;"><span>    List&lt;Enemy&gt; results)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    results.Clear();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">float</span> radiusSquared = radius * radius;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i = <span style="color:#ae81ff">0</span>; i &lt; enemies.Count; i++)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Enemy enemy = enemies[i];
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (enemy != <span style="color:#66d9ef">null</span> &amp;&amp;
</span></span><span style="display:flex;"><span>            (enemy.transform.position - origin).sqrMagnitude &lt;= radiusSquared)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            results.Add(enemy);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This version is less mathematically fluent, but game-loop ownership is clearer: the caller supplies storage and knows that the method clears it. Document that mutation because it is part of the contract.</p>
<p>Do not rewrite every query into a buffer API pre-emptively. Profile the player build and optimize code that runs often enough to matter. The corrective point is narrower: extension syntax does not make allocations disappear, so inspect the implementation before putting a pleasant chain in a hot path.</p>
<h2 id="choose-namespaces-as-part-of-the-api">Choose namespaces as part of the API</h2>
<p>Extension methods become candidates through namespace imports. Namespace design therefore controls discoverability and collision risk.</p>
<p>Putting every helper in a root namespace such as <code>Game</code> makes all extensions appear almost everywhere. That feels convenient until unrelated methods crowd autocomplete or two packages define the same name for the same receiver.</p>
<p>Group extensions by stable technical concern:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Game.Foundation.Collections
</span></span><span style="display:flex;"><span>Game.Foundation.Unity
</span></span><span style="display:flex;"><span>Game.Editor.AssetPipeline
</span></span><span style="display:flex;"><span>Game.Combat.Targeting
</span></span></code></pre></div><p>Keep editor-only extensions in an editor assembly, not merely behind scattered compiler directives. Runtime code should not see helpers that depend on <code>UnityEditor</code>. Assembly definition files provide a stronger boundary than naming alone and prevent accidental player-build references.</p>
<p>Avoid generic class names such as <code>Extensions</code> repeated across many files. The containing class is rarely visible at the call site, but it matters in stack traces, static-call syntax, code search, and conflict diagnosis. <code>TransformExtensions</code>, <code>LayerMaskExtensions</code>, and <code>SerializedPropertyExtensions</code> state their scope plainly.</p>
<p>Public packages need even more restraint. An extension imported by a package consumer can conflict with that consumer&rsquo;s own vocabulary. Keep package extensions in a package-specific namespace and expose only the ones that carry real semantic value. <code>internal</code> is a good default for helpers that exist solely to tidy one assembly.</p>
<h2 id="name-methods-for-outcomes-not-tricks">Name methods for outcomes, not tricks</h2>
<p>Good extension names survive after the implementation is forgotten. They describe a result, a mutation, or a query with normal C# conventions.</p>
<p>A few distinctions matter:</p>
<ul>
<li>Use <code>Try...</code> when failure is expected and returned as a Boolean.</li>
<li>Use <code>Get...</code> for retrieval, not creation or global search unless the rest of the API already establishes that convention.</li>
<li>Use <code>GetOrAdd...</code> when the operation may mutate the receiver.</li>
<li>Use <code>Collect...</code> or <code>Copy...To</code> when caller-owned output is filled.</li>
<li>Use <code>To...</code> when a conversion commonly creates a new value.</li>
<li>Add <code>Async</code> when the method returns asynchronous work.</li>
<li>Reserve <code>Require...</code> for invariant checks that fail loudly.</li>
</ul>
<p>Avoid names such as <code>Safe</code>, <code>Smart</code>, <code>Fast</code>, and <code>Better</code>. They do not define behaviour. <code>SafeDestroy</code> is especially unhelpful: safe with respect to Edit mode, delayed destruction, null inputs, undo support, or asset objects? A method that has different runtime and editor behaviour should state the actual policy and live in the appropriate assembly.</p>
<p>Boolean helpers should read as predicates. <code>mask.Contains(layer)</code> is clear. <code>layer.IsIn(mask)</code> can also work, but it makes an integer the subject and spreads game-specific vocabulary across a primitive type. Prefer the receiver that owns the concept.</p>
<h2 id="ref-extensions-and-mutation-need-extra-care">Ref extensions and mutation need extra care</h2>
<p>C# permits extension methods that receive some value types by reference. They can mutate the caller&rsquo;s variable rather than a copy. This is useful in narrow data-oriented code, but surprising mutation is worse than a few extra characters.</p>
<p>For ordinary Unity vectors, returning a changed copy is usually clear:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> Vector3 WithY(<span style="color:#66d9ef">this</span> Vector3 <span style="color:#66d9ef">value</span>, <span style="color:#66d9ef">float</span> y)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">value</span>.y = y;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">value</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Usage makes assignment visible:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>velocity = velocity.WithY(jumpSpeed);
</span></span></code></pre></div><p>A <code>ref this Vector3</code> extension could mutate <code>velocity</code> in place, but that syntax is easy to mistake for normal instance behaviour and does not work on every temporary or property value. The returned-copy form follows Unity&rsquo;s existing value-type style and composes without hidden mutation.</p>
<p>For a project-owned performance-critical struct, a <code>ref</code> extension may be justified after measurement. Put it in a narrow namespace, use a mutation verb, and test it. Do not create a general ref-based vocabulary for Unity structs merely because the language allows it.</p>
<h2 id="test-extension-methods-as-ordinary-static-functions">Test extension methods as ordinary static functions</h2>
<p>Small, deterministic extensions should have small, deterministic tests. They do not need a scene when the contract is pure.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> NUnit.Framework;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LayerMaskExtensionsTests</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [Test]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Contains_ReturnsTrueOnlyForIncludedLayer()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        LayerMask mask = (<span style="color:#ae81ff">1</span> &lt;&lt; <span style="color:#ae81ff">3</span>) | (<span style="color:#ae81ff">1</span> &lt;&lt; <span style="color:#ae81ff">7</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        Assert.That(mask.Contains(<span style="color:#ae81ff">3</span>), Is.True);
</span></span><span style="display:flex;"><span>        Assert.That(mask.Contains(<span style="color:#ae81ff">7</span>), Is.True);
</span></span><span style="display:flex;"><span>        Assert.That(mask.Contains(<span style="color:#ae81ff">2</span>), Is.False);
</span></span><span style="display:flex;"><span>        Assert.That(mask.Contains(-<span style="color:#ae81ff">1</span>), Is.False);
</span></span><span style="display:flex;"><span>        Assert.That(mask.Contains(<span style="color:#ae81ff">32</span>), Is.False);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Call pure methods with static syntax in tests when that makes the mechanism clearer:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>Assert.That(LayerMaskExtensions.Contains(mask, <span style="color:#ae81ff">3</span>), Is.True);
</span></span></code></pre></div><p>For helpers around <code>GameObject</code>, component lookup, or destroyed-object semantics, use an Edit Mode test that creates and destroys the minimum object graph. Check both ordinary null and a destroyed Unity object when the method claims to handle Unity objects.</p>
<p>Performance-sensitive extensions need a different check. Profile representative calls in a player build, inspect managed allocations, and compare the extension with the direct operation it replaces. Microbenchmarks can isolate a helper, but frame impact still depends on call frequency and surrounding engine work.</p>
<p>The method&rsquo;s small size is not a reason to skip verification. Small helpers spread widely, so a mistaken edge case gets multiplied across the codebase.</p>
<h2 id="review-the-call-site-then-review-the-hidden-static-call">Review the call site, then review the hidden static call</h2>
<p>Extension syntax is successful when both views look reasonable.</p>
<p>First, read the fluent call:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (damageableLayers.Contains(other.gameObject.layer))
</span></span><span style="display:flex;"><span>    damageQueue.Add(hit);
</span></span></code></pre></div><p>It should read naturally and put the operation&rsquo;s real subject on the left.</p>
<p>Then mentally expand it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (LayerMaskExtensions.Contains(
</span></span><span style="display:flex;"><span>        damageableLayers,
</span></span><span style="display:flex;"><span>        other.gameObject.layer))
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    damageQueue.Add(hit);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That form should still have an honest parameter list. If the static version reveals that the method actually needs global services, mutates distant state, allocates multiple collections, or performs a large search, the fluent version is hiding too much.</p>
<p>A useful code-review checklist is short:</p>
<ol>
<li>Does the receiver own the concept?</li>
<li>Is the cost unsurprising from the name?</li>
<li>Are dependencies passed explicitly?</li>
<li>Are Unity null semantics handled intentionally?</li>
<li>Is mutation visible?</li>
<li>Is the namespace narrow enough?</li>
<li>Does an existing .NET or Unity API already say this clearly?</li>
<li>Would a normal service, component, or plain static method be more honest?</li>
</ol>
<p>The seventh question deletes a surprising number of helpers. Unity already provides <code>TryGetComponent</code>; C# already provides collection and null operations; project code does not need aliases for every API whose name is a few characters longer than preferred.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Extension methods are best used as a thin layer of vocabulary, not a hidden architecture.</p>
<p>Choose a receiver that is genuinely the subject of the operation. Keep the method narrow, deterministic where possible, and explicit about searches, allocations, mutation, asynchronous work, and failure. Respect Unity&rsquo;s special object lifetime and null behaviour. Use constrained types and focused namespaces instead of making universal helpers appear on every object. Keep editor helpers in editor assemblies and package helpers in package-specific namespaces.</p>
<p>Most importantly, expand the call in your head. If the static version would look dishonest because it hides services or substantial work, extension syntax does not improve it. If the static version is a small operation whose first argument clearly owns the concept, the extension can make gameplay code easier to read without making it harder to reason about.</p>
<p>That is the standard worth keeping: clearer at the call site, equally clear underneath.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods">Microsoft Learn: Extension members</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/extension-methods">Microsoft Learn: Extension member design guidelines</a></li>
<li><a href="https://docs.unity3d.com/ScriptReference/Object.html">Unity Scripting API: Object</a></li>
<li><a href="https://docs.unity3d.com/ScriptReference/Component.TryGetComponent.html">Unity Scripting API: Component.TryGetComponent</a></li>
<li><a href="https://docs.unity3d.com/Manual/performance-garbage-collection-best-practices.html">Unity Manual: Garbage collection best practices</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Marshalling Between C# and C&#43;&#43; in Unity: Design the Boundary First</title>
            <link>https://blog.jonmikeys.com/posts/marshalling-csharp-cpp-unity/</link>
            <pubDate>Mon, 21 Sep 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/marshalling-csharp-cpp-unity/</guid>
            <description>A practical guide to moving data safely between Unity C# and C++ without letting ABI details, ownership mistakes, or hidden copies destabilize the game.</description>
            <content type="html"><![CDATA[<p>Calling C++ from Unity looks easy in the smallest example. Export a function, add <code>DllImport</code> in C#, and pass a few numbers across. The call works, so the boundary appears finished.</p>
<p>The difficult bugs arrive later. A Boolean occupies a different number of bytes than expected. A struct gains a field and its alignment changes. A native library keeps a callback after the managed delegate has been collected. An error message points to memory that was already released. Everything behaves in the Editor, then an IL2CPP build fails because ahead-of-time compilation has different constraints.</p>
<p>Those failures all point to a broken contract at the language boundary.</p>
<p>Marshalling is the work required to move values between managed C# and unmanaged native code. Some values can cross unchanged. Others need conversion, copying, pinning, allocation, or an explicit lifetime rule. A reliable Unity plug-in keeps that contract narrow enough to inspect and predictable enough to test.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Unity gameplay code
</span></span><span style="display:flex;"><span>        |
</span></span><span style="display:flex;"><span>        v
</span></span><span style="display:flex;"><span>managed wrapper  -- validation, ownership, Unity-friendly types
</span></span><span style="display:flex;"><span>        |
</span></span><span style="display:flex;"><span>        v
</span></span><span style="display:flex;"><span>C ABI            -- fixed widths, explicit buffers, error codes
</span></span><span style="display:flex;"><span>        |
</span></span><span style="display:flex;"><span>        v
</span></span><span style="display:flex;"><span>C++ implementation -- classes, templates, threads, native libraries
</span></span></code></pre></div><p>Most of the integration work belongs in the managed wrapper and C ABI. The <code>DllImport</code> declaration only describes the call.</p>
<h2 id="export-c-implement-c">Export C, implement C++</h2>
<p>C++ is a poor binary interface between independently built modules. Function names can be mangled, exceptions depend on compatible runtimes, class layouts are compiler-sensitive, and standard-library types such as <code>std::string</code> or <code>std::vector</code> are implementation details rather than portable wire formats. A small C-compatible API keeps those compiler-specific details inside the plug-in.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cpp" data-lang="cpp"><span style="display:flex;"><span><span style="color:#75715e">// game_native.h
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e">#pragma once
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdint.h&gt;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#if defined(_WIN32)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">#define GAME_API extern &#34;C&#34; __declspec(dllexport)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#else
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">#define GAME_API extern &#34;C&#34; __attribute__((visibility(&#34;default&#34;)))
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#endif
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">NativePoint</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">float</span> x;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">float</span> y;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">float</span> z;
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Returns the number of output points, or a negative error code.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>GAME_API <span style="color:#66d9ef">int32_t</span> <span style="color:#a6e22e">path_smooth</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> NativePoint<span style="color:#f92672">*</span> input,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int32_t</span> input_count,
</span></span><span style="display:flex;"><span>    NativePoint<span style="color:#f92672">*</span> output,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int32_t</span> output_capacity);
</span></span></code></pre></div><p>The implementation behind <code>path_smooth</code> can use classes, templates, SIMD code, or a third-party navigation library. None of those choices leak into C#. The exported surface uses fixed-width integers, pointers, a plain struct, and a documented return convention.</p>
<p><code>extern &quot;C&quot;</code> prevents C++ name mangling. The export macro makes the symbol visible from the dynamic library. The fixed-width <code>int32_t</code> avoids the platform-dependent meaning of types such as <code>long</code>, and the caller-owned buffers keep memory ownership explicit.</p>
<p>Treat the exported header as a protocol definition. Once builds ship against it, changing field order or parameter meaning is an ABI change even if the C++ compiler accepts the edit.</p>
<h2 id="match-the-signature-exactly">Match the signature exactly</h2>
<p>The corresponding Unity declaration can stay small:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Runtime.InteropServices;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">[StructLayout(LayoutKind.Sequential)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">internal</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">NativePoint</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> X;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> Y;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> Z;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">internal</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">GameNative</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">string</span> Library = <span style="color:#e6db74">&#34;game_native&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [DllImport(Library,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">        EntryPoint = &#34;path_smooth&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">        CallingConvention = CallingConvention.Cdecl,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">        ExactSpelling = true)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">internal</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">extern</span> <span style="color:#66d9ef">int</span> PathSmooth(
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">        [In]</span> NativePoint[] input,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">int</span> inputCount,
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">        [Out]</span> NativePoint[] output,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">int</span> outputCapacity);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The declaration must describe the C function exactly: parameter order, widths, signedness, calling convention, layout, and direction.</p>
<p>Microsoft recommends <code>LibraryImport</code> for modern .NET 7-or-newer applications when source-generated interop is available. Unity projects still need to account for the scripting runtime, Unity version, selected backend, and target platforms they actually ship. <code>DllImport</code> remains the conservative choice across many Unity configurations because the engine&rsquo;s supported toolchain matters more than general .NET fashion.</p>
<p>Keep all imports in one internal class. Gameplay code should call a managed wrapper, not scatter P/Invoke declarations through MonoBehaviours. That wrapper can validate lengths, reuse buffers, turn error codes into useful exceptions, and convert between <code>Vector3</code> and the boundary type.</p>
<h2 id="prefer-data-that-is-already-the-right-shape">Prefer data that is already the right shape</h2>
<p>A blittable type has the same bit-level representation on both sides of the boundary. Values such as <code>byte</code>, <code>int</code>, <code>uint</code>, <code>long</code>, <code>float</code>, <code>double</code>, pointers, and fixed-layout structs made only from blittable fields are the easiest values to pass. The runtime does not need to translate each field into another representation.</p>
<p>That is why <code>NativePoint</code> contains three <code>float</code> fields rather than a managed object reference. Sequential layout makes the field order explicit. The native and managed definitions still need an agreement about packing and alignment, but this shape is straightforward on the usual Unity targets.</p>
<p>A few innocent-looking types deserve suspicion:</p>
<ul>
<li>C# <code>bool</code> does not have one universal native representation. A C++ <code>bool</code> is commonly one byte, while default P/Invoke rules can map a managed Boolean to a four-byte Windows <code>BOOL</code>.</li>
<li>C and C++ <code>long</code> change width across common platforms. C# <code>long</code> is always 64 bits.</li>
<li><code>char</code> and <code>wchar_t</code> do not provide one portable text encoding across Windows, Linux, and macOS.</li>
<li>Enums need an explicitly agreed underlying integer type.</li>
<li>References, strings, arrays, and nested managed objects are not plain bytes with self-evident ownership.</li>
</ul>
<p>A stable plug-in interface can avoid most custom attributes by using <code>int32_t</code> status values, <code>uint8_t</code> flags, fixed-width enum storage, and UTF-8 byte sequences. Explicit types remove entire categories of platform bugs.</p>
<p>If the native API cannot be changed and truly exposes a one-byte Boolean, say so in C#:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[return: MarshalAs(UnmanagedType.U1)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">internal</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">extern</span> <span style="color:#66d9ef">bool</span> IsReady();
</span></span></code></pre></div><p>For a new API, returning <code>int32_t</code> and interpreting zero or nonzero in the managed wrapper is often even clearer.</p>
<h2 id="arrays-need-a-length-and-a-direction">Arrays need a length and a direction</h2>
<p>A pointer does not carry its element count. Every array crossing a C boundary therefore needs a length, and every output buffer needs a capacity.</p>
<p>The <code>path_smooth</code> contract follows a useful pattern:</p>
<ol>
<li>C# allocates or rents the output array.</li>
<li>C# passes the input length and output capacity.</li>
<li>C++ never writes beyond that capacity.</li>
<li>C++ returns the number of elements written or a negative error code.</li>
</ol>
<p>That pattern avoids returning a native allocation whose owner is unclear. It also lets the managed side reuse arrays across frames instead of creating garbage for every call.</p>
<p>The convenient <code>[In]</code> and <code>[Out]</code> array signature is a good starting point for occasional or coarse-grained work. A high-frequency call may need <code>unsafe</code> code with pinned spans or direct pointers to remove marshaller decisions and intermediate copies. Do not start there without a measurement. A single native call processing 2,000 points is usually a better boundary than 2,000 calls processing one point each.</p>
<p>Crossing the boundary has fixed overhead. More importantly, conversion and copying can dominate tiny native operations. Batch work so native code does enough useful computation to justify the trip.</p>
<h2 id="strings-are-bytes-plus-an-encoding-contract">Strings are bytes plus an encoding contract</h2>
<p><code>char*</code> does not mean “string” with enough precision for an ABI. It means a pointer to character-sized storage. The contract must also define:</p>
<ul>
<li>the encoding</li>
<li>whether the data is null-terminated or length-delimited</li>
<li>whether null is allowed</li>
<li>who allocated the memory</li>
<li>how long the pointer remains valid</li>
<li>who releases it</li>
</ul>
<p>UTF-8 is the practical cross-platform default for a new Unity plug-in. It avoids the platform differences around <code>wchar_t</code> and matches Unity&rsquo;s guidance for string values returned by native methods on iOS.</p>
<p>Input strings that native code only reads during the call can be declared with explicit UTF-8 marshalling:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[DllImport(Library, CallingConvention = CallingConvention.Cdecl)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">internal</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">extern</span> <span style="color:#66d9ef">int</span> LoadRegion(
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [MarshalAs(UnmanagedType.LPUTF8Str)]</span> <span style="color:#66d9ef">string</span> path);
</span></span></code></pre></div><p>Output strings deserve a stricter design. Returning <code>const char*</code> is safe only if the lifetime is documented and the managed side copies it before the pointer becomes invalid. Returning an allocated <code>char*</code> requires a matching native free function, and that free function must use the same allocator that created the memory.</p>
<p>A caller-owned buffer is often easier to reason about:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cpp" data-lang="cpp"><span style="display:flex;"><span><span style="color:#75715e">// Returns required bytes including the null terminator.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// Writes only when output_capacity is large enough.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>GAME_API <span style="color:#66d9ef">int32_t</span> <span style="color:#a6e22e">get_last_error</span>(<span style="color:#66d9ef">char</span><span style="color:#f92672">*</span> output, <span style="color:#66d9ef">int32_t</span> output_capacity);
</span></span></code></pre></div><p>C# can ask for the required size, rent a <code>byte[]</code>, call again, and decode the bytes as UTF-8. There is more ceremony, but ownership is unambiguous. It also avoids relying on <code>StringBuilder</code>, whose marshalling can create multiple buffers and copies.</p>
<p>Never release native memory with <code>Marshal.FreeHGlobal</code> unless the native API explicitly allocated it using the matching mechanism. “Both are unmanaged memory” is not an ownership contract.</p>
<h2 id="opaque-handles-keep-c-objects-private">Opaque handles keep C++ objects private</h2>
<p>Sometimes the plug-in needs long-lived native state: a pathfinding world, audio processor, compression context, or hardware connection. Passing a C++ class pointer directly and pretending C# understands the object is the wrong abstraction.</p>
<p>Expose an opaque handle instead:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cpp" data-lang="cpp"><span style="display:flex;"><span><span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">NavWorld</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>GAME_API NavWorld<span style="color:#f92672">*</span> <span style="color:#a6e22e">nav_world_create</span>(<span style="color:#66d9ef">const</span> NativePoint<span style="color:#f92672">*</span> vertices, <span style="color:#66d9ef">int32_t</span> count);
</span></span><span style="display:flex;"><span>GAME_API <span style="color:#66d9ef">int32_t</span> <span style="color:#a6e22e">nav_world_query</span>(NavWorld<span style="color:#f92672">*</span> world, NativePoint from, NativePoint to);
</span></span><span style="display:flex;"><span>GAME_API <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">nav_world_destroy</span>(NavWorld<span style="color:#f92672">*</span> world);
</span></span></code></pre></div><p>C# sees the pointer as an <code>IntPtr</code> or, preferably, a <code>SafeHandle</code> wrapper. It cannot inspect the C++ object, and it should not try. Its responsibilities are to reject a null result, keep the handle alive during calls, and release it exactly once through <code>nav_world_destroy</code>.</p>
<p><code>SafeHandle</code> is preferable to hand-written finalization because it exists specifically to model native resource ownership. An <code>IDisposable</code> managed façade can own the safe handle and provide Unity-friendly methods. The rest of the project never needs to see <code>IntPtr</code>.</p>
<p>The destroy function is part of the ABI, not optional cleanup. Native leaks are especially painful in the Editor because disabled domain reload settings and repeated play sessions can make lifetime mistakes appear inconsistent.</p>
<h2 id="callbacks-reverse-every-lifetime-assumption">Callbacks reverse every lifetime assumption</h2>
<p>A callback lets native code call a managed function pointer. It is useful for progress reports, log messages, asynchronous completion, and events from native middleware. It also introduces three separate constraints: signature, delegate lifetime, and thread affinity.</p>
<p>A Unity-compatible callback shape commonly looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> AOT;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Runtime.InteropServices;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">internal</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">NativeCallbacks</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">internal</span> <span style="color:#66d9ef">delegate</span> <span style="color:#66d9ef">void</span> LogCallback(<span style="color:#66d9ef">int</span> level, IntPtr utf8Message);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Root the delegate for as long as native code may call it.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> LogCallback Log = OnNativeLog;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [DllImport(&#34;game_native&#34;, CallingConvention = CallingConvention.Cdecl)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">extern</span> <span style="color:#66d9ef">void</span> RegisterLogCallback(LogCallback callback);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">internal</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">void</span> Register() =&gt; RegisterLogCallback(Log);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [MonoPInvokeCallback(typeof(LogCallback))]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">void</span> OnNativeLog(<span style="color:#66d9ef">int</span> level, IntPtr utf8Message)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Copy the message, then enqueue it for the Unity main thread.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Do not call UnityEngine APIs if the native caller may be a worker thread.</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>For AOT builds, Unity documents static callback methods marked with <code>MonoPInvokeCallback</code>. The exact delegate signature and calling convention still have to match the native function pointer.</p>
<p>The static field keeps the delegate alive after the registration call returns. Native code may store that function pointer and invoke it much later. If managed code drops its last delegate reference, the native library can call into collected state. Keep the delegate rooted until native code unregisters it and can no longer issue callbacks.</p>
<p>A callback also runs on whichever thread invoked it. If a native worker thread reports completion, the callback is on that worker thread. Most Unity APIs belong on the main thread, so the callback should copy the payload into managed-owned data and enqueue it. <code>Update</code> or another main-thread pump can consume the queue.</p>
<p>Do not let exceptions escape a reverse P/Invoke callback. Catch them at the managed boundary, record the failure, and return a defined status if the callback signature permits one.</p>
<h2 id="errors-must-cross-as-data-not-exceptions">Errors must cross as data, not exceptions</h2>
<p>C++ exceptions should stop before the exported C function returns. Allowing one to unwind through managed frames is undefined territory and can terminate the process.</p>
<p>Catch exceptions inside the native wrapper and convert them into a stable error code:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cpp" data-lang="cpp"><span style="display:flex;"><span>GAME_API <span style="color:#66d9ef">int32_t</span> <span style="color:#a6e22e">path_smooth</span>(<span style="color:#75715e">/* arguments */</span>)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Validate pointers and capacities, then call the C++ implementation.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#66d9ef">return</span> perform_path_smoothing(<span style="color:#75715e">/* arguments */</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">catch</span> (<span style="color:#66d9ef">const</span> std<span style="color:#f92672">::</span>bad_alloc<span style="color:#f92672">&amp;</span>)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">2</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">catch</span> (...)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The managed façade can translate <code>-1</code> into a <code>NativePluginException</code>, preserve <code>-2</code> as an out-of-memory failure, or return a normal result object. Both sides still need to agree on what each value means.</p>
<p>Validation belongs on both sides. C# should reject mismatched counts before paying for a native call. C++ should still reject null pointers, negative lengths, and impossible capacities because the native boundary is a trust boundary. A bad pointer does not produce a friendly Unity exception; it can take down the player.</p>
<h2 id="editor-success-does-not-prove-player-success">Editor success does not prove player success</h2>
<p>Unity native plug-ins vary by platform. Windows loads a DLL, macOS typically uses a bundle or dynamic library, Linux uses a shared object, and iOS commonly links native source or a static library into the player. On statically linked Apple targets, the import name is commonly <code>&quot;__Internal&quot;</code> rather than a file name.</p>
<p>The Unity Inspector also controls which platforms and CPU architectures import a plug-in. A correct C# declaration cannot load an x86-64 library into an ARM64 player. File placement, import settings, exported symbol visibility, architecture, and dependent native libraries all belong to the deployment contract.</p>
<p>IL2CPP adds an AOT step, so test the actual backend early. Callback preservation, unsupported generic paths, and platform linker behavior are not exercised by an Editor session running Mono. A minimal development player that loads the plug-in and performs one call catches more useful failures than another hour staring at attributes.</p>
<p>Keep platform selection close to the interop class:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#75715e">#if</span> UNITY_IOS &amp;&amp; !UNITY_EDITOR
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">string</span> Library = <span style="color:#e6db74">&#34;__Internal&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#75715e">#else</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">string</span> Library = <span style="color:#e6db74">&#34;game_native&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#75715e">#endif</span>
</span></span></code></pre></div><p>Conditional compilation should choose the library location, not hide incompatible ABI designs. The exported function and data contract should remain the same wherever possible.</p>
<h2 id="put-version-and-size-checks-in-the-api">Put version and size checks in the API</h2>
<p>Interop failures are often silent layout disagreements. Add cheap probes that make those disagreements loud:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cpp" data-lang="cpp"><span style="display:flex;"><span>GAME_API <span style="color:#66d9ef">uint32_t</span> <span style="color:#a6e22e">interop_abi_version</span>();
</span></span><span style="display:flex;"><span>GAME_API <span style="color:#66d9ef">int32_t</span> <span style="color:#a6e22e">interop_sizeof_native_point</span>();
</span></span></code></pre></div><p>In a development build, the managed wrapper can compare the returned ABI version with its expected constant and compare native <code>sizeof(NativePoint)</code> with <code>Marshal.SizeOf&lt;NativePoint&gt;()</code>. Fail at startup with a useful message rather than corrupting a buffer during a match.</p>
<p>A small boundary test should cover:</p>
<ul>
<li>minimum and maximum integer values used by the API</li>
<li>a struct round trip with distinctive field values</li>
<li>empty, one-element, and full-capacity arrays</li>
<li>non-ASCII UTF-8 text</li>
<li>a deliberately undersized output buffer</li>
<li>every documented error code</li>
<li>callback registration, invocation, and unregistration</li>
<li>repeated create/destroy cycles for opaque handles</li>
<li>the Editor and at least one IL2CPP development player</li>
</ul>
<p>The test does not need a framework. A tiny scene or command-line test program that asserts these cases is enough. Boundary tests are valuable because they fail near the mismatch instead of several systems later.</p>
<h2 id="keep-the-boundary-coarse-and-dull">Keep the boundary coarse and dull</h2>
<p>A native plug-in earns its maintenance cost when it provides something C# cannot reasonably provide: existing middleware, platform APIs, specialized codecs, hardware integration, or measured performance in a substantial computation. Choosing C++ only because it sounds faster is weak justification. Per-object native calls from <code>Update</code>, marshalled strings in a hot loop, and tiny functions that bounce across the ABI can erase the expected gain. Native code also adds builds, symbols, deployment rules, crash debugging, and architecture-specific artifacts.</p>
<p>Start with one managed façade per native subsystem. Batch inputs. Reuse buffers. Keep exported functions few. Keep C++ objects behind handles. Make ownership visible in function names and documentation. Measure the whole call path in a player build, including conversion and copying, rather than timing only the inner native algorithm.</p>
<p>A good boundary is deliberately less expressive than either language. It uses plain data because plain data survives compiler changes, backend changes, and platform changes.</p>
<h2 id="check-the-contract-before-adding-another-export">Check the contract before adding another export</h2>
<p>Before adding the next native function, write its contract beside the declaration: exact widths and layout, buffer bounds, string encoding, memory ownership, error behavior, and callback lifetime or thread rules. Add one boundary test that calls it from an IL2CPP development player. If any of those rules remain unclear, the function is not ready to ship.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://docs.unity3d.com/Manual/plug-ins-native.html">Unity Manual: Native plug-ins</a></li>
<li><a href="https://docs.unity3d.com/Manual/ios-native-plugin-call-back.html">Unity Manual: Create callbacks from native code</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/standard/native-interop/best-practices">Microsoft Learn: Native interoperability best practices</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/standard/native-interop/type-marshalling">Microsoft Learn: Type marshalling</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/framework/interop/marshalling-a-delegate-as-a-callback-method">Microsoft Learn: Marshalling a delegate as a callback method</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Decorator Pattern in Games: Stack Behavior Without Subclass Explosion</title>
            <link>https://blog.jonmikeys.com/posts/decorator-pattern-in-games/</link>
            <pubDate>Thu, 17 Sep 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/decorator-pattern-in-games/</guid>
            <description>Use decorators to stack optional game behavior around a stable interface, while keeping order, lifetime, allocation, and debugging costs visible.</description>
            <content type="html"><![CDATA[<p>Game features rarely stay singular. A weapon deals damage, then gains critical hits, elemental damage, life steal, analytics, tutorial restrictions, or a temporary event rule. The first addition is easy. The fifth exposes whether the original design can carry optional behavior without becoming a matrix of special cases.</p>
<p>Inheritance handles one fixed variation reasonably well. It handles combinations badly. <code>FireSword</code>, <code>CriticalFireSword</code>, and <code>LifeStealCriticalFireSword</code> are not a healthy class hierarchy; they are a list of feature permutations disguised as types. A boolean-heavy base class is not much better. It moves the combinations into branches and leaves one object responsible for every possible rule.</p>
<p>The decorator pattern offers another shape. A decorator implements the same interface as the object it wraps. It forwards the operation to that wrapped object while adding work before or after the call. Because a decorator can wrap another decorator, small behaviors can be assembled into a stack.</p>
<p>That is the attractive part. The corrective part is that decorators are not a universal status-effect system. Wrapper order matters, runtime inspection can be awkward, and careless assembly can allocate far too often. The pattern works best when the operation has a stable interface and the optional layers have clear ownership.</p>
<h2 id="the-useful-definition">The useful definition</h2>
<p>A decorator has three defining properties:</p>
<ol>
<li>It implements the same contract as the wrapped component.</li>
<li>It stores another implementation of that contract.</li>
<li>It delegates to the wrapped implementation while adding behavior.</li>
</ol>
<p>The shared contract is what makes recursive composition possible. Code expecting an <code>IAttack</code> can receive a plain attack, one wrapped in a critical-hit rule, or a stack containing critical damage, fire damage, and life steal. The caller still invokes one method.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>caller
</span></span><span style="display:flex;"><span>  -&gt; life steal
</span></span><span style="display:flex;"><span>      -&gt; critical hit
</span></span><span style="display:flex;"><span>          -&gt; basic weapon attack
</span></span><span style="display:flex;"><span>      &lt;- damage result
</span></span><span style="display:flex;"><span>  &lt;- heal from actual damage dealt
</span></span></code></pre></div><p>Work can happen while the request travels inward, while the result travels outward, or both. A critical decorator may modify the attack request before forwarding it. A life-steal decorator may wait for the inner attack to finish, then heal from the damage actually dealt.</p>
<p>That two-way flow is more important than the usual “wrapper” metaphor. It lets a layer transform input, observe output, enforce a condition, or add a side effect without changing the caller or the base operation.</p>
<h2 id="start-with-one-narrow-gameplay-operation">Start with one narrow gameplay operation</h2>
<p>A decorator interface should describe one cohesive operation. Avoid starting with a broad <code>IGameObject</code> interface containing movement, rendering, damage, saving, audio, and interaction. Any wrapper implementing that contract would either forward dozens of unrelated members or quietly become another god object.</p>
<p>An attack-resolution seam is narrow enough for a useful example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IAttack</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    DamageResult Execute(AttackContext context);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">AttackContext</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> AttackContext(Actor source, Actor target)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Source = source;
</span></span><span style="display:flex;"><span>        Target = target;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Actor Source { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Actor Target { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">DamageResult</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> DamageResult(<span style="color:#66d9ef">int</span> attempted, <span style="color:#66d9ef">int</span> dealt)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Attempted = attempted;
</span></span><span style="display:flex;"><span>        Dealt = dealt;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> Attempted { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> Dealt { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>DamageResult</code> deserves to exist even in a small example. Downstream behavior usually needs the result of resolution, not merely the requested amount. Armor, immunity, shields, and clamping can all make actual damage lower than attempted damage. Life steal based on the original number is a common bug.</p>
<p>The concrete component performs the ordinary operation:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">WeaponAttack</span> : IAttack
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">int</span> damage;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> WeaponAttack(<span style="color:#66d9ef">int</span> damage)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (damage &lt; <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ArgumentOutOfRangeException(nameof(damage));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.damage = damage;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> DamageResult Execute(AttackContext context)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">int</span> dealt = context.Target.TakeDamage(damage);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> DamageResult(damage, dealt);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This class should remain boring. It is the center of the composition, not a registry of every optional rule the game may add later.</p>
<h2 id="a-base-decorator-should-only-remove-repetition">A base decorator should only remove repetition</h2>
<p>The formal version of the pattern often includes an abstract base decorator. In C#, that class is useful when it removes forwarding boilerplate. It should not become a second inheritance framework.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">AttackDecorator</span> : IAttack
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> AttackDecorator(IAttack inner)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Inner = inner ?? <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ArgumentNullException(nameof(inner));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> IAttack Inner { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">abstract</span> DamageResult Execute(AttackContext context);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A critical-hit layer can now make a decision and adjust the result. For clarity, this example receives randomness through an interface rather than calling a global random generator. That makes the rule deterministic in tests and lets a server-authoritative simulation provide its own random stream.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IRandomSource</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">float</span> Next01();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CriticalAttack</span> : AttackDecorator
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IRandomSource random;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">float</span> chance;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">int</span> multiplier;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> CriticalAttack(
</span></span><span style="display:flex;"><span>        IAttack inner,
</span></span><span style="display:flex;"><span>        IRandomSource random,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">float</span> chance,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">int</span> multiplier) : <span style="color:#66d9ef">base</span>(inner)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (chance &lt; <span style="color:#ae81ff">0f</span> || chance &gt; <span style="color:#ae81ff">1f</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ArgumentOutOfRangeException(nameof(chance));
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (multiplier &lt; <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ArgumentOutOfRangeException(nameof(multiplier));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.random = random ?? <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ArgumentNullException(nameof(random));
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.chance = chance;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.multiplier = multiplier;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">override</span> DamageResult Execute(AttackContext context)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        DamageResult result = Inner.Execute(context);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (random.Next01() &gt;= chance)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> result;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">int</span> bonusRequested = result.Attempted * (multiplier - <span style="color:#ae81ff">1</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">int</span> bonusDealt = context.Target.TakeDamage(bonusRequested);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> DamageResult(
</span></span><span style="display:flex;"><span>            result.Attempted + bonusRequested,
</span></span><span style="display:flex;"><span>            result.Dealt + bonusDealt);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>There are several valid ways to model a critical hit. It could multiply a damage request before mitigation instead of issuing bonus damage afterward. It could affect only physical damage, or it could duplicate all child effects. Those choices are game rules, not pattern details.</p>
<p>The example deliberately makes the choice visible. The decorator does not make combat semantics correct by itself. It only gives the rule a composable home.</p>
<h2 id="result-side-behavior-is-where-wrapping-becomes-valuable">Result-side behavior is where wrapping becomes valuable</h2>
<p>Life steal demonstrates why a returned result is useful:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LifeStealAttack</span> : AttackDecorator
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">float</span> fraction;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> LifeStealAttack(IAttack inner, <span style="color:#66d9ef">float</span> fraction) : <span style="color:#66d9ef">base</span>(inner)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (fraction &lt; <span style="color:#ae81ff">0f</span> || fraction &gt; <span style="color:#ae81ff">1f</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ArgumentOutOfRangeException(nameof(fraction));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.fraction = fraction;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">override</span> DamageResult Execute(AttackContext context)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        DamageResult result = Inner.Execute(context);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">int</span> healing = (<span style="color:#66d9ef">int</span>)MathF.Floor(result.Dealt * fraction);
</span></span><span style="display:flex;"><span>        context.Source.Heal(healing);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> result;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The layer does not need to understand armor, shields, or how the attack produced damage. It depends on one explicit fact: the amount actually dealt. This is a good decorator boundary because the added behavior is local and the inner implementation remains interchangeable.</p>
<p>Assembly happens at a composition root such as an equipment factory, character loadout builder, or match setup system:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>IAttack attack = <span style="color:#66d9ef">new</span> WeaponAttack(damage: <span style="color:#ae81ff">20</span>);
</span></span><span style="display:flex;"><span>attack = <span style="color:#66d9ef">new</span> CriticalAttack(attack, combatRandom, chance: <span style="color:#ae81ff">0.25f</span>, multiplier: <span style="color:#ae81ff">2</span>);
</span></span><span style="display:flex;"><span>attack = <span style="color:#66d9ef">new</span> LifeStealAttack(attack, fraction: <span style="color:#ae81ff">0.20f</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>DamageResult result = attack.Execute(<span style="color:#66d9ef">new</span> AttackContext(player, enemy));
</span></span></code></pre></div><p>The calling code sees one <code>IAttack</code>. The stack can be configured once when equipment changes rather than reconstructed for every swing.</p>
<p>That last point matters in a game loop. The pattern creates object graphs. If code builds a fresh chain on every shot, every AI tick, or every card preview, the architectural neatness may arrive with avoidable garbage collection. Compose on loadout changes, pool where measured, or use value-oriented pipelines for truly hot paths.</p>
<h2 id="order-is-part-of-the-design">Order is part of the design</h2>
<p>Decorators look modular, but they are not automatically independent. The order of wrappers can change the result.</p>
<p>Consider these two stacks:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>LifeSteal(Critical(BaseAttack))
</span></span><span style="display:flex;"><span>Critical(LifeSteal(BaseAttack))
</span></span></code></pre></div><p>In the first stack, life steal sees the combined result returned by the critical layer. In the second, life steal completes before the outer critical layer applies its bonus. The player heals for different amounts.</p>
<p>The same issue appears with elemental conversion, clamping, shields, logging, cooldown gates, and analytics. A logger outside the stack observes the final result. A logger inside observes only what happened beneath it. A clamp before a multiplier differs from a clamp after it.</p>
<p>Treat ordering as a game rule rather than an incidental constructor detail. A practical project can define a small number of phases:</p>
<ul>
<li>validation and permission checks</li>
<li>input conversion</li>
<li>base operation</li>
<li>result modifiers</li>
<li>side effects</li>
<li>observation and reporting</li>
</ul>
<p>Not every game needs a general pipeline framework. Often one clearly named factory method and a test for the expected order are enough:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> IAttack BuildVampireBlade(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> damage,
</span></span><span style="display:flex;"><span>    IRandomSource random)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    IAttack attack = <span style="color:#66d9ef">new</span> WeaponAttack(damage);
</span></span><span style="display:flex;"><span>    attack = <span style="color:#66d9ef">new</span> CriticalAttack(attack, random, <span style="color:#ae81ff">0.25f</span>, <span style="color:#ae81ff">2</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> LifeStealAttack(attack, <span style="color:#ae81ff">0.20f</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>If designers must reorder dozens of effects at runtime, a list of explicit modifiers with phases and priorities may be more transparent than nested wrappers. Decorator is a composition technique, not an excuse to hide ordering policy.</p>
<h2 id="do-not-confuse-decorators-with-stat-modifiers">Do not confuse decorators with stat modifiers</h2>
<p>A stat system often needs to answer questions that a wrapper chain answers poorly:</p>
<ul>
<li>Which effects currently contribute to this value?</li>
<li>Remove the modifier from item instance 417.</li>
<li>Stack poison up to five times, then refresh its duration.</li>
<li>Serialize all active effects.</li>
<li>Show each contribution in a tooltip.</li>
<li>Recalculate only when a dependency changes.</li>
</ul>
<p>Those requirements point toward data records in a collection, not anonymous layers in a recursive object graph. A list of modifiers can be filtered, sorted, grouped, saved, and displayed directly.</p>
<p>Decorators are a better fit when behavior must surround an operation. Examples include retrying a request, measuring an operation, gating an ability, adding a post-hit effect, caching a query, or translating the input and output of a service.</p>
<p>The distinction is not “numbers versus behavior.” It is about the operations the rest of the game needs. If removal by identity, UI explanation, duration, and persistence are central, model those concepts explicitly. Do not force them through wrappers because the initial class diagram looks tidy.</p>
<p>A hybrid is common. ScriptableObject assets or plain data records describe effects. A factory reads those definitions and creates a runtime attack stack for a loadout. The persistent model remains inspectable, while the runtime operation remains small.</p>
<h2 id="decorator-strategy-command-and-chain-are-not-synonyms">Decorator, strategy, command, and chain are not synonyms</h2>
<p>Several patterns use composition and can look similar in C#.</p>
<p><strong>Strategy</strong> swaps the implementation of an operation. A flying movement strategy replaces a walking movement strategy. Decorator keeps an implementation and surrounds it with additional behavior. Strategy changes the core choice; decorator layers optional responsibilities around a compatible core.</p>
<p><strong>Command</strong> represents a request as data or an object that can be queued, recorded, or undone. A command may call a decorated service, but the command itself does not need to wrap another command with the same interface.</p>
<p><strong>Chain of responsibility</strong> passes a request among handlers, often allowing one handler to consume it or stop traversal. A decorator normally preserves one logical component and delegates through every configured layer. A permission decorator may reject an operation, but if early termination is the system&rsquo;s main idea, a handler chain may describe the code more honestly.</p>
<p><strong>Adapter</strong> changes one interface into another. Decorator preserves the component interface so wrapped and unwrapped objects remain interchangeable.</p>
<p>Pattern names matter less than intent. Use the name that explains why the indirection exists. Future maintainers need to know whether an object replaces behavior, records intent, translates an API, or adds a layer around an existing operation.</p>
<h2 id="unity-specific-assembly-choices">Unity-specific assembly choices</h2>
<p>Plain C# objects are usually the easiest decorators to test. They do not need to be <code>MonoBehaviour</code> components merely because the game runs in Unity. A <code>MonoBehaviour</code> can own the lifetime of the assembled stack and pass Unity services into it.</p>
<p>Avoid relying on constructors for <code>MonoBehaviour</code> or <code>ScriptableObject</code> instances created by Unity. Use constructors for ordinary C# runtime objects. Use serialized fields, factory methods, <code>OnEnable</code>, or an explicit initialization path for Unity-managed objects, depending on the lifetime involved.</p>
<p>ScriptableObjects are useful as effect definitions shared by many instances. They are assets, however, so mutable per-character state should not quietly live on a shared definition. A poison asset can hold base duration and tick damage. The remaining duration for one enemy belongs in a runtime object.</p>
<p>A small definition might expose a factory method:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">AttackEffectDefinition</span> : ScriptableObject
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">abstract</span> IAttack Wrap(IAttack inner, AttackServices services);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>An equipment asset can hold an ordered list of definitions. At runtime, the loadout system starts with the base attack and applies each definition in order. This gives designers an editable composition while keeping mutable combat state outside the assets.</p>
<p>There is still a tradeoff. An inspector list makes order editable but does not prove that every order is meaningful. Add validation for known incompatible phases, display the effective order in a debug view, and keep the supported combinations narrower than the theoretical ones.</p>
<h2 id="debugging-a-stack-should-not-require-archaeology">Debugging a stack should not require archaeology</h2>
<p>A wrapper chain hides concrete types behind one interface. That is helpful to callers and occasionally miserable during debugging. A breakpoint on <code>Execute</code> may jump through six wrappers before reaching the base operation. Logs may report the outer type while omitting the rules inside it.</p>
<p>Solve this at the assembly boundary rather than putting reflection into combat code. The factory can record a readable description of the stack for development builds. A debug inspector can show the ordered effect definitions that produced it. Tests can construct known stacks directly.</p>
<p>Do not expose the mutable <code>Inner</code> reference publicly just so arbitrary systems can peel wrappers apart. That breaks encapsulation and creates removal logic that depends on graph surgery. If effects must be added and removed frequently by identity, that is more evidence that the game needs a modifier collection or explicit effect container.</p>
<p>Observability decorators are useful too. A timing or logging layer can surround an operation without polluting the operation itself. Keep such layers out of release builds when their cost is not justified, and avoid logging every call in a hot loop.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<h3 id="creating-a-subclass-for-each-combination-anyway">Creating a subclass for each combination anyway</h3>
<p>A decorator should reduce combinatorial types. If the project still defines a dedicated assembled subclass for every weapon and effect combination, little has been gained. Put configuration in the composition root or data definitions.</p>
<h3 id="rebuilding-the-chain-on-every-operation">Rebuilding the chain on every operation</h3>
<p>Wrappers are normally assembled when configuration changes. Rebuilding them per frame or per attack creates avoidable allocations and repeats work that does not change.</p>
<h3 id="hiding-mandatory-rules-in-optional-wrappers">Hiding mandatory rules in optional wrappers</h3>
<p>Core invariants should not depend on somebody remembering to attach the correct decorator. If every damage operation must clamp health and respect invulnerability, that belongs in the authoritative damage receiver or another unavoidable boundary.</p>
<h3 id="letting-wrappers-own-unrelated-systems">Letting wrappers own unrelated systems</h3>
<p>An attack decorator that directly updates quests, audio, camera shake, achievements, UI, and persistence has become a coordination hub. Return a useful result or publish a narrow event after resolution so those systems can react through their proper boundaries.</p>
<h3 id="assuming-all-wrappers-commute">Assuming all wrappers commute</h3>
<p>Most do not. Multiplication, addition, clamping, conversion, and side effects can produce different outcomes in different orders. Tests should lock down the intended order with concrete values.</p>
<h3 id="using-decorators-where-a-function-is-enough">Using decorators where a function is enough</h3>
<p>A one-off calculation does not need a class hierarchy. A delegate, local function, or direct call may express the same rule with less machinery. Decorator earns its place when the shared interface, recursive composition, and runtime assembly are all useful.</p>
<h2 id="a-small-test-that-catches-the-real-risks">A small test that catches the real risks</h2>
<p>The important tests are not “is this class a decorator?” They are behavioral checks for result propagation and order.</p>
<p>For the vampire-blade example, use a fixed random source and actors with known armor. Then verify:</p>
<ol>
<li>a non-critical attack reports and heals from actual damage dealt;</li>
<li>a forced critical attack includes the bonus in the outer life-steal calculation;</li>
<li>moving life steal inside the critical layer produces the intentionally different result;</li>
<li>zero damage produces zero healing;</li>
<li>the stack is created once and reused across several attacks.</li>
</ol>
<p>A compact assertion can document the ordering rule:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>IAttack attack = <span style="color:#66d9ef">new</span> WeaponAttack(<span style="color:#ae81ff">20</span>);
</span></span><span style="display:flex;"><span>attack = <span style="color:#66d9ef">new</span> CriticalAttack(attack, <span style="color:#66d9ef">new</span> AlwaysCritical(), <span style="color:#ae81ff">1f</span>, <span style="color:#ae81ff">2</span>);
</span></span><span style="display:flex;"><span>attack = <span style="color:#66d9ef">new</span> LifeStealAttack(attack, <span style="color:#ae81ff">0.25f</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>DamageResult result = attack.Execute(<span style="color:#66d9ef">new</span> AttackContext(hero, unarmoredTarget));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Debug.Assert(result.Dealt == <span style="color:#ae81ff">40</span>);
</span></span><span style="display:flex;"><span>Debug.Assert(hero.HealthGained == <span style="color:#ae81ff">10</span>);
</span></span></code></pre></div><p>That test is more valuable than checking the number of wrapper objects or their inheritance relationships. It fails when a refactor changes combat behavior, which is the risk worth guarding.</p>
<h2 id="when-the-pattern-is-the-right-size">When the pattern is the right size</h2>
<p>Decorator is a good fit when all of the following are true:</p>
<ul>
<li>callers should depend on one stable interface;</li>
<li>optional behavior belongs before or after one operation;</li>
<li>layers need to be combined at runtime;</li>
<li>inheritance would create feature combinations;</li>
<li>the stack is assembled infrequently compared with how often it runs;</li>
<li>order can be defined and tested clearly.</li>
</ul>
<p>Prefer a simpler direct call when there is only one optional rule. Prefer strategy when one implementation replaces another. Prefer a data-oriented modifier collection when effects need identity, duration, sorting, removal, serialization, or rich UI inspection. Prefer a purpose-built pipeline when many ordered stages and early exits are the central design.</p>
<p>The practical takeaway is narrow: decorate operations, not entire game objects. Keep the interface small, compose the stack at a visible boundary, make ordering explicit, and test the result that travels back out. Used that way, decorators remove subclass combinations without replacing them with an invisible maze of wrappers.</p>
<h2 id="references">References</h2>
<ul>
<li>Gamma, Helm, Johnson, and Vlissides, <em>Design Patterns: Elements of Reusable Object-Oriented Software</em>, “Decorator.”</li>
<li><a href="https://refactoring.guru/design-patterns/decorator">Refactoring.Guru: Decorator</a></li>
<li><a href="https://docs.unity3d.com/Manual/class-ScriptableObject.html">Unity Manual: ScriptableObject</a></li>
<li><a href="https://gameprogrammingpatterns.com/component.html">Game Programming Patterns: Component</a> — useful background on favoring smaller composed domains over a monolithic game-object class.</li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Octrees for Faster 3D Pathfinding</title>
            <link>https://blog.jonmikeys.com/posts/octrees-for-faster-3d-pathfinding/</link>
            <pubDate>Thu, 17 Sep 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/octrees-for-faster-3d-pathfinding/</guid>
            <description>Build a smaller 3D navigation graph by giving open space large octree cells and reserving fine detail for obstacles, then run A* over the free leaves.</description>
            <content type="html"><![CDATA[<p>Pathfinding becomes expensive when the navigation representation describes the world at the same resolution everywhere.</p>
<p>That is manageable on a small two-dimensional grid. It becomes much less attractive in a genuinely three-dimensional space. A volume that is 256 cells wide, 256 cells deep, and 64 cells high contains more than four million cells before the search has expanded a single node. Most of those cells may describe empty air, yet they still consume memory and enlarge the graph.</p>
<p>An octree attacks the representation problem first. Instead of dividing the whole world into equally sized cells, it keeps large cells where the space is simple and subdivides only where more detail is needed. A hangar can become a handful of large navigable regions while the geometry around beams, doors, and machinery receives finer cells.</p>
<p>A* does not become a different algorithm. It simply searches a smaller, adaptive graph.</p>
<p><img src="/images/posts/octrees-for-faster-3d-pathfinding/diagram.svg" alt="An adaptive octree turns open volume into a small navigation graph while retaining fine cells around obstacles."></p>
<h2 id="why-a-uniform-3d-grid-wastes-work">Why a uniform 3D grid wastes work</h2>
<p>A uniform grid has useful properties. Every cell has the same size, indexing is straightforward, and finding a neighbour is often a small coordinate calculation. Those advantages make grids a strong choice for tile maps and constrained tactical spaces.</p>
<p>The cost is that resolution is global. If a narrow doorway needs half-metre cells, the entire search volume pays for half-metre cells. Large open chambers, empty sky, and long tunnels are all represented with the same density as the doorway.</p>
<p>Doubling resolution along one axis doubles the cell count in two dimensions. In three dimensions it affects the volume. Halving the cell width can produce roughly eight times as many cells because twice as many cells are needed along each of the three axes.</p>
<p>That growth affects more than storage:</p>
<ul>
<li>A* may place more nodes in its open and closed sets.</li>
<li>Building connectivity takes longer.</li>
<li>Dynamic obstacle updates touch more cells.</li>
<li>Per-cell clearance, cost, or tactical data consumes more memory.</li>
<li>Cache behaviour worsens when the search jumps through a large node set.</li>
</ul>
<p>A better priority queue cannot erase a graph that is unnecessarily large. The strongest optimisation may be to stop creating so many equivalent nodes.</p>
<h2 id="what-an-octree-represents">What an octree represents</h2>
<p>An octree starts with one axis-aligned cube covering the navigable world. That cube is the root. When the root needs more detail, it is divided into eight equal children: lower and upper halves along the x, y, and z axes.</p>
<p>Each child can be divided again. The process continues until a cell is simple enough to keep or reaches the chosen minimum size.</p>
<p>For navigation, a node commonly receives one of three classifications:</p>
<ul>
<li><strong>Free:</strong> the volume can be traversed.</li>
<li><strong>Blocked:</strong> the volume cannot be traversed.</li>
<li><strong>Mixed:</strong> it contains both free space and obstacle geometry, so it may need subdivision.</li>
</ul>
<p>Only the leaves matter to the final navigation graph. A large free leaf represents a large open volume. A cluster of small leaves represents detailed space near geometry. Blocked leaves are omitted from the traversable graph.</p>
<p>This gives the octree its main advantage: spatial resolution follows geometric complexity rather than world size.</p>
<p>The classification test must account for the moving agent, not only point occupancy. A cell that does not intersect a wall may still be too close to the wall for a wide drone. One practical method is to expand obstacles by the agent radius before classifying cells. Another is to store clearance and reject leaves or transitions that do not meet the current agent&rsquo;s requirements.</p>
<p>The first method produces a graph tailored to one agent size. The second can support several sizes but makes queries and edge validation more involved.</p>
<h2 id="the-octree-is-not-yet-a-pathfinding-graph">The octree is not yet a pathfinding graph</h2>
<p>Subdivision answers where free space exists. A pathfinder also needs connections.</p>
<p>Create a graph node for each traversable leaf, then connect leaves that share a passable boundary. This sounds simple until adjacent leaves have different sizes. One large leaf may touch four or sixteen smaller leaves across one face. A fixed six-neighbour lookup is no longer enough.</p>
<p>A robust edge-building pass needs to answer three questions:</p>
<ol>
<li>Do the leaf bounds touch on a face rather than only at an edge or corner?</li>
<li>Does the shared face contain enough open area for the agent to cross?</li>
<li>Is the movement valid under the game&rsquo;s rules for slope, clearance, gravity, and direction?</li>
</ol>
<p>For a flying agent, any clear shared face may be usable. A walking character usually needs a support surface, acceptable vertical change, and enough headroom. An underwater vehicle may need turning clearance in addition to body clearance.</p>
<p>This distinction matters because “3D pathfinding” covers very different movement models. An octree is a spatial representation, not permission to move freely in every direction.</p>
<p>Neighbour links can be built eagerly after the tree is generated or discovered lazily during search. Eager links make queries cheaper and easier to inspect, but rebuilding them can be expensive when the world changes. Lazy discovery reduces stored edges and can suit sparse queries, but neighbour lookup must be efficient or it simply moves the cost into A*.</p>
<h2 id="running-a-over-octree-leaves">Running A* over octree leaves</h2>
<p>Once free leaves and valid transitions exist, ordinary A* can search them. The open set still prioritises the node with the smallest estimated total cost:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>f(n) = g(n) + h(n)
</span></span></code></pre></div><p><code>g(n)</code> is the cost paid from the start. <code>h(n)</code> estimates the cost from the current leaf to the goal.</p>
<p>For free-flying movement with distance-based costs, Euclidean distance between positions is an admissible heuristic. The simplest positions are leaf centres, but centre-to-centre costs can produce awkward routes when cell sizes differ. A route may enter a large cell near one corner and be charged as if it travelled through the centre.</p>
<p>There are several ways to improve this:</p>
<ul>
<li>Keep leaf centres for the search, then smooth the resulting route.</li>
<li>Place transition portals on shared faces and evaluate movement between portals.</li>
<li>Estimate edge cost from the closest valid points on neighbouring cell bounds.</li>
<li>Use the centre graph only for the high-level route, then refine movement locally.</li>
</ul>
<p>The centre-and-smooth approach is a useful starting point. Portal-based search is more precise, especially where large cells meet narrow passages, but it requires more geometry work.</p>
<p>If the heuristic never overestimates the remaining cost, A* retains its optimality guarantee relative to the graph it was given. That final phrase is important. A* can find the cheapest route through an imperfect octree graph and still produce a poor world-space path. Search quality cannot exceed representation quality.</p>
<p>For a fuller explanation of open sets, heuristics, and path reconstruction, see <a href="/posts/astar-pathfinding/">A* Pathfinding: A Practical Guide for Game Developers</a>.</p>
<h2 id="where-the-speedup-comes-from">Where the speedup comes from</h2>
<p>The useful comparison is not “octree lookup versus array lookup.” Array lookup will often be cheaper.</p>
<p>The octree wins when fewer nodes and edges compensate for more complicated spatial operations. A large free region that would occupy thousands of voxel cells may become one leaf. A* can cross that region in one graph step instead of expanding a wave through every voxel.</p>
<p>Suppose a warehouse contains open air around a few shelves and gantries. A dense voxel graph spends most of its memory proving that empty space is empty. An octree keeps large leaves in the clear volume and refines around shelf edges, the floor, and narrow routes. Search effort becomes concentrated around decisions rather than distance alone.</p>
<p>That does not mean the route should jump directly from the centre of one large cell to another. The graph route identifies a corridor of free leaves. A smoothing or portal pass then finds a better polyline through that corridor.</p>
<p>Three measurements usually reveal whether the representation is helping:</p>
<ul>
<li>total traversable leaf count compared with uniform-grid cell count</li>
<li>nodes expanded per representative query</li>
<li>end-to-end query time, including neighbour discovery and smoothing</li>
</ul>
<p>Memory use and rebuild time should be recorded as well. A fast query structure that takes too long to update may be unsuitable for a destructible world.</p>
<h2 id="a-practical-build-pipeline">A practical build pipeline</h2>
<p>A navigation octree can be built with a compact recursive process:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Build(cell, depth):
</span></span><span style="display:flex;"><span>    occupancy = Classify(cell)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    if occupancy is Free:
</span></span><span style="display:flex;"><span>        emit traversable leaf
</span></span><span style="display:flex;"><span>        return
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    if occupancy is Blocked:
</span></span><span style="display:flex;"><span>        emit blocked leaf
</span></span><span style="display:flex;"><span>        return
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    if depth == maxDepth:
</span></span><span style="display:flex;"><span>        apply conservative fallback
</span></span><span style="display:flex;"><span>        return
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    for each of the cell&#39;s eight children:
</span></span><span style="display:flex;"><span>        Build(child, depth + 1)
</span></span></code></pre></div><p>The conservative fallback normally marks an unresolved mixed cell as blocked. That may remove valid space, but it prevents the pathfinder from routing an agent through geometry the representation could not resolve. If valid passages disappear, the correct response is to increase local resolution or improve classification rather than pretend the cell is free.</p>
<p>After subdivision:</p>
<ol>
<li>collect free leaves</li>
<li>build or prepare neighbour lookup</li>
<li>locate the leaves containing the start and goal</li>
<li>run A* over free leaves</li>
<li>reconstruct the leaf corridor</li>
<li>convert the corridor to world-space points</li>
<li>smooth the points while preserving collision clearance</li>
</ol>
<p>Keeping these stages separate makes the system easier to profile. It also prevents an expensive smoothing pass from being blamed on the octree search itself.</p>
<h2 id="finding-neighbours-without-scanning-every-leaf">Finding neighbours without scanning every leaf</h2>
<p>The naive neighbour algorithm compares every leaf with every other leaf. It is easy to understand and becomes unusable as the leaf count grows.</p>
<p>The tree already contains the information needed for a better lookup. A face-neighbour query can move upward until it finds an ancestor with space across the requested face, step into the sibling on that side, then descend into children touching the opposite face. The result may be one leaf of the same size, one larger leaf, or a set of smaller leaves.</p>
<p>Another practical option is a hash map keyed by coordinates at the finest resolution. A leaf registers the range or boundary keys it occupies, and a face query retrieves candidates from adjacent keys. This spends extra memory to simplify lookup.</p>
<p>There is no universal winner. Tree-walking keeps the data structure self-contained. Spatial hashing can be easier to debug and faster for frequent queries. Eager adjacency is the simplest runtime option when the octree is mostly static.</p>
<p>Whichever method is chosen, neighbour results should be cached if repeated searches traverse the same static tree. Repeating the same hierarchical lookup for every agent throws away much of the benefit of preprocessing.</p>
<h2 id="coarse-to-fine-search">Coarse-to-fine search</h2>
<p>Searching leaf nodes directly is already adaptive, but the hierarchy can do more.</p>
<p>A coarse-to-fine pathfinder first finds a broad route through higher-level regions, then refines only the parts that need detail. This resembles hierarchical pathfinding on grids: solve the large-scale routing problem before spending time on local geometry.</p>
<p>One approach is to create entrances between sibling regions and search an abstract graph of those entrances. Another is to allow high-level free nodes to participate directly while mixed regions expose lower-level descendants.</p>
<p>The gain is largest in wide worlds with long routes. A path from one end of a space station to the other should not inspect every maintenance duct between them. The high-level search can choose a sequence of sectors, then detailed searches can connect the relevant entrances.</p>
<p>The tradeoff is complexity and path quality. Abstract edges need useful costs. If they ignore local congestion or narrow passages, the high-level route may choose a region that looks short but refines poorly. Start with free-leaf A*. Add another hierarchy only after profiles show that leaf-level searches are still too expensive.</p>
<h2 id="dynamic-obstacles-and-partial-rebuilding">Dynamic obstacles and partial rebuilding</h2>
<p>Octrees are often described as ideal for dynamic worlds because branches can be updated independently. That is directionally true, not automatic.</p>
<p>When an obstacle changes, find the affected leaves, reclassify the overlapping region, split cells that became mixed, and attempt to merge sibling leaves that became uniformly free or blocked. Then repair adjacency around the changed boundary.</p>
<p>The merge rule is important. Eight sibling leaves with the same classification can collapse into their parent only if doing so preserves required metadata such as clearance, traversal cost, or ownership. Without merging, repeated edits gradually leave the tree over-subdivided.</p>
<p>Dynamic updates also introduce consistency problems. A query must not traverse half-rebuilt connectivity. Common solutions include:</p>
<ul>
<li>rebuilding a small region synchronously before allowing new queries</li>
<li>swapping in an immutable rebuilt branch</li>
<li>versioning nodes and rejecting stale paths</li>
<li>queuing path requests until an update completes</li>
</ul>
<p>Moving characters should rarely cause structural octree edits. Treating every NPC as permanent blocked geometry creates constant churn and can eliminate routes in crowded spaces. Use local avoidance, reservation systems, or time-aware planning for transient agents. Reserve octree updates for geometry or obstacles that remain relevant long enough to justify rebuilding navigation data.</p>
<h2 id="path-smoothing-is-not-optional-polish">Path smoothing is not optional polish</h2>
<p>An adaptive graph often creates a more obvious need for smoothing than a uniform grid. Leaf centres can zigzag as the path crosses differently sized cells. The route is valid but may look mechanical and travel farther than necessary.</p>
<p>A simple string-pulling pass starts at one waypoint and tests whether a later waypoint can be reached directly. If the swept agent shape stays inside free space, intermediate points can be removed. Repeat until no more points can be skipped.</p>
<p>Use a volume cast or clearance-aware segment test rather than a zero-width ray. A ray can pass through a gap that the agent cannot.</p>
<p>For a corridor represented by shared face portals, a funnel-style algorithm can produce a cleaner path, although a general three-dimensional funnel is less straightforward than its navmesh counterpart. Many games use a simpler compromise: derive candidate points from portals, apply collision-aware shortcutting, and let steering handle small deviations.</p>
<p>Smoothing must use the same agent assumptions as graph construction. If the octree was built for a sphere with radius 0.5 metres, the shortcut test must not validate a centre line with no radius.</p>
<h2 id="traversal-costs-still-matter">Traversal costs still matter</h2>
<p>Fewer nodes do not mean all free space is equivalent.</p>
<p>A leaf can carry costs for slope, exposure, terrain, current, noise, or threat. Large leaves need care because one value describes more volume. If a large cell contains meaningful cost variation, it may need subdivision even when it is geometrically free.</p>
<p>This extends the split criterion beyond collision:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>split when geometry is mixed
</span></span><span style="display:flex;"><span>or traversal cost varies too much
</span></span><span style="display:flex;"><span>or tactical data needs more detail
</span></span></code></pre></div><p>That creates a navigation structure whose resolution follows decision complexity. A broad, safe chamber can stay coarse. A geometrically open kill zone with strong exposure gradients may need finer leaves so the pathfinder can distinguish the safer edge from the dangerous centre.</p>
<p>This idea connects naturally to layered spatial data. The earlier article <a href="/posts/spatial-databases-for-tactical-ai/">Spatial Databases for Tactical AI</a> discusses keeping signals such as visibility, cover, and occupancy separate. Those layers can influence octree subdivision or edge costs without collapsing every concern into the tree itself.</p>
<p>Keep the collision representation authoritative. Tactical layers may adjust preferences, but they should not quietly turn blocked space into a valid route.</p>
<h2 id="a-small-c-data-shape">A small C# data shape</h2>
<p>The runtime node does not need to mirror every build-time detail. A compact leaf record can hold bounds, occupancy, and cached neighbours:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">OctreeLeaf</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Bounds Bounds { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">bool</span> IsTraversable { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> TraversalMultiplier { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IReadOnlyList&lt;OctreeLeaf&gt; Neighbours =&gt; neighbours;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> List&lt;OctreeLeaf&gt; neighbours = <span style="color:#66d9ef">new</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> OctreeLeaf(
</span></span><span style="display:flex;"><span>        Bounds bounds,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">bool</span> isTraversable,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">float</span> traversalMultiplier = <span style="color:#ae81ff">1f</span>)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Bounds = bounds;
</span></span><span style="display:flex;"><span>        IsTraversable = isTraversable;
</span></span><span style="display:flex;"><span>        TraversalMultiplier = traversalMultiplier;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Connect(OctreeLeaf other)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (!IsTraversable || !other.IsTraversable || other == <span style="color:#66d9ef">this</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (!neighbours.Contains(other))
</span></span><span style="display:flex;"><span>            neighbours.Add(other);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Production code may store indices instead of object references for denser memory and better cache behaviour. The important boundary is conceptual: subdivision builds leaves, connectivity validates transitions, and A* consumes the resulting graph. Keeping those jobs distinct makes each one testable.</p>
<p>At minimum, tests should cover:</p>
<ul>
<li>a completely free root that remains one leaf</li>
<li>a blocked root with no traversable node</li>
<li>a mixed root that subdivides</li>
<li>a large leaf connected to several smaller leaves across one face</li>
<li>a narrow opening rejected for an oversized agent</li>
<li>an update that splits and later merges the same branch</li>
<li>a path whose smoothed segments retain clearance</li>
</ul>
<p>The unequal-neighbour case deserves special attention. Many octree navigation bugs hide there because same-size cells work correctly during early prototypes.</p>
<h2 id="when-octrees-are-the-wrong-choice">When octrees are the wrong choice</h2>
<p>Octrees are useful when the world is volumetric, sparse, and unevenly detailed. Flying agents, underwater vehicles, zero-gravity movement, voxel caves, and large destructible volumes are natural candidates.</p>
<p>They are less compelling when movement is constrained to surfaces. A navmesh usually represents walkable floors more directly and produces better paths with fewer special rules for support and slopes. A two-dimensional grid may be simpler for a tactics game whose height differences are discrete. A waypoint graph may be enough for a small authored level.</p>
<p>Octrees also lose some appeal when the environment is dense at every scale. If almost every cell remains mixed until the maximum depth, the tree approaches the leaf count of a uniform voxel grid while retaining more complicated neighbour logic and pointer overhead.</p>
<p>The decision should be driven by measurements from representative levels:</p>
<ol>
<li>Build a simple uniform baseline.</li>
<li>Record memory, build time, node count, expansions, and query latency.</li>
<li>Build the octree at equivalent minimum clearance.</li>
<li>Include neighbour lookup and smoothing in the timing.</li>
<li>Test short, long, open, narrow, and unreachable routes.</li>
<li>Test representative world updates if geometry changes at runtime.</li>
</ol>
<p>If the octree reduces expansions but total query time rises, neighbour discovery or memory layout may be the actual bottleneck. If queries are fast but updates hitch, partial rebuilding needs attention. If paths cut corners, the problem is likely transition validation or smoothing rather than A*.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Octrees optimise 3D pathfinding by reducing the graph before the search begins. Open volumes become large leaves. Detailed geometry receives small leaves. A* then spends its effort around obstacles and route choices instead of expanding thousands of equivalent empty voxels.</p>
<p>The design succeeds when four details are handled carefully:</p>
<ul>
<li>free leaves are classified for the agent&rsquo;s real volume</li>
<li>neighbours across unequal cell sizes are connected correctly</li>
<li>edge costs and heuristics match the movement model</li>
<li>the leaf corridor is converted into a clearance-safe world-space path</li>
</ul>
<p>Start with leaf-level A*, cached adjacency, and collision-aware smoothing. Measure that version before adding coarse-to-fine abstractions or highly dynamic rebuilding. The octree already provides the largest structural gain: it lets simple space stay simple.</p>
]]></content>
        </item>
        
        <item>
            <title>Unity CullingGroup: Stop Updating What the Player Cannot Notice</title>
            <link>https://blog.jonmikeys.com/posts/cullinggroup-for-smarter-object-management/</link>
            <pubDate>Sun, 13 Sep 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/cullinggroup-for-smarter-object-management/</guid>
            <description>Use Unity&amp;rsquo;s CullingGroup API to turn camera visibility and distance bands into practical simulation budgets for crowds, effects, spawn points, and world objects.</description>
            <content type="html"><![CDATA[<p>Large Unity scenes often waste CPU time for a simple reason: every object behaves as if the player is paying attention to it.</p>
<p>An NPC behind a building runs the same animation graph as one standing in front of the camera. A distant ambient effect keeps updating. Every possible spawn point checks its surroundings. Hundreds of world markers measure their distance from the player in separate <code>Update</code> methods.</p>
<p>None of those decisions is individually disastrous. Their combined cost is.</p>
<p>Unity&rsquo;s <code>CullingGroup</code> API offers a useful middle ground between doing nothing and replacing the whole project with a custom spatial database. Give it bounding spheres, a camera, and a set of distance thresholds. Unity then reports when each sphere changes visibility or moves into another distance band. Gameplay code can use those changes to select a cheaper level of simulation.</p>
<p>The important correction is that <code>CullingGroup</code> does not manage objects. It supplies coarse visibility and distance information. The game still decides what “cheap,” “dormant,” and “visible” mean.</p>
<h2 id="culling-renderers-is-only-half-the-job">Culling renderers is only half the job</h2>
<p>Unity already avoids drawing many objects outside the camera frustum. Static occlusion culling can also reject renderers hidden behind baked occluders. That saves rendering work, but it does not automatically settle every gameplay cost attached to those objects.</p>
<p>A disabled renderer does not imply that an AI planner, animation controller, IK pass, audio emitter, or custom <code>Update</code> method has stopped. Those systems have different correctness rules. An off-screen guard may still need to hear a sound. A distant animal may only need a cheap movement approximation. A hidden spawn point may be especially useful because spawning there will not visibly pop an enemy into existence.</p>
<p>This is where <code>CullingGroup</code> fits. It exposes information from Unity&rsquo;s culling pipeline so non-rendering systems can make deliberate budget decisions too.</p>
<p>Typical uses include:</p>
<ul>
<li>reducing animation and AI quality for distant crowds</li>
<li>pausing cosmetic effects when their area is not visible</li>
<li>finding hidden spawn locations</li>
<li>waking world objects when the player approaches</li>
<li>replacing hundreds of per-object distance checks with one grouped query</li>
<li>keeping only a lightweight data representation for distant agents</li>
</ul>
<p>This is not a promise that every use will be faster. It is a way to move repeated visibility and proximity work into a purpose-built engine facility, then respond only when state changes.</p>
<h2 id="the-data-model-is-intentionally-coarse">The data model is intentionally coarse</h2>
<p>A culling group works with an array of <code>BoundingSphere</code> values. Each sphere has a center and radius. There are no boxes, capsules, meshes, or arbitrary volumes.</p>
<p>That limitation is useful. A sphere is cheap to test and has no orientation. The tradeoff is an approximate fit. A tall building enclosed by one large sphere may count as visible when only empty space around its corners intersects the view. A long train may need several spheres if one enclosing sphere is too loose.</p>
<p>The group calculates two related results for every active sphere:</p>
<ol>
<li><strong>Visibility</strong> relative to one target camera.</li>
<li><strong>A distance band</strong> relative to one reference point.</li>
</ol>
<p>Visibility uses camera-frustum culling and static occlusion-culling information. Unity&rsquo;s documentation is explicit that dynamic objects are not considered as occluders. A truck driving between the camera and an NPC does not make that NPC hidden as far as this API is concerned.</p>
<p>Distance is measured from the reference point to the closest point on the sphere, not simply to its center. Unity does not return the raw distance. It compares the result with thresholds supplied by the game and returns a discrete band.</p>
<p>Given thresholds of 20, 60, and 120 metres, the bands can be interpreted as:</p>
<ul>
<li>band 0: close</li>
<li>band 1: medium</li>
<li>band 2: far</li>
<li>beyond the last threshold: outside the managed range</li>
</ul>
<p>That quantization is a feature, not missing precision. Most gameplay budgeting does not need to know that an agent is exactly 47.38 metres away. It needs to know that the agent crossed from “full simulation” into “reduced simulation.”</p>
<h2 id="pick-budgets-before-writing-callbacks">Pick budgets before writing callbacks</h2>
<p>The API is easier to use when the policy is defined first. Otherwise every callback grows a collection of unrelated toggles and exceptions.</p>
<p>A practical crowd policy might look like this:</p>
<table>
<thead>
<tr>
<th>State</th>
<th>AI</th>
<th>Animation</th>
<th>Cosmetic effects</th>
</tr>
</thead>
<tbody>
<tr>
<td>close and visible</td>
<td>full rate</td>
<td>full</td>
<td>enabled</td>
</tr>
<tr>
<td>close and hidden</td>
<td>full or reduced</td>
<td>disabled</td>
<td>disabled</td>
</tr>
<tr>
<td>medium</td>
<td>low-frequency decisions</td>
<td>visible agents only</td>
<td>disabled</td>
</tr>
<tr>
<td>far</td>
<td>coarse movement only</td>
<td>disabled</td>
<td>disabled</td>
</tr>
<tr>
<td>outside managed range</td>
<td>dormant record</td>
<td>disabled</td>
<td>disabled</td>
</tr>
</tbody>
</table>
<p>The exact table depends on the game. Multiplayer authority, audio propagation, combat, and deterministic simulation can all require work that visibility alone cannot switch off.</p>
<p>The useful pattern is to separate <strong>simulation significance</strong> from <strong>presentation visibility</strong>. Distance is often a reasonable input to simulation quality. Camera visibility is often a reasonable input to animation, particles, and other presentation work. Combining both into one <code>SetActive(false)</code> call is tempting and usually wrong.</p>
<h2 id="a-small-working-setup">A small working setup</h2>
<p>The following component owns one culling group for a fixed set of agents. It updates sphere positions in <code>LateUpdate</code> and sends state changes back to each agent.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">AgentCullingController</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> Camera targetCamera;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> Transform distanceReference;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> CullableAgent[] agents;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">float</span> sphereRadius = <span style="color:#ae81ff">1.5f</span>;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">float</span>[] distanceBands = { <span style="color:#ae81ff">20f</span>, <span style="color:#ae81ff">60f</span>, <span style="color:#ae81ff">120f</span> };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> CullingGroup <span style="color:#66d9ef">group</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> BoundingSphere[] spheres;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnEnable()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (targetCamera == <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>            targetCamera = Camera.main;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (targetCamera == <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            Debug.LogError(<span style="color:#e6db74">&#34;AgentCullingController needs a target camera.&#34;</span>, <span style="color:#66d9ef">this</span>);
</span></span><span style="display:flex;"><span>            enabled = <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        spheres = <span style="color:#66d9ef">new</span> BoundingSphere[agents.Length];
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i = <span style="color:#ae81ff">0</span>; i &lt; agents.Length; i++)
</span></span><span style="display:flex;"><span>            spheres[i] = <span style="color:#66d9ef">new</span> BoundingSphere(agents[i].transform.position, sphereRadius);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">group</span> = <span style="color:#66d9ef">new</span> CullingGroup
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            targetCamera = targetCamera,
</span></span><span style="display:flex;"><span>            onStateChanged = OnStateChanged
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">group</span>.SetBoundingSpheres(spheres);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">group</span>.SetBoundingSphereCount(spheres.Length);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">group</span>.SetBoundingDistances(distanceBands);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">group</span>.SetDistanceReferencePoint(
</span></span><span style="display:flex;"><span>            distanceReference != <span style="color:#66d9ef">null</span> ? distanceReference : targetCamera.transform);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> LateUpdate()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// CullingGroup keeps a reference to this array, so changing entries is enough.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i = <span style="color:#ae81ff">0</span>; i &lt; agents.Length; i++)
</span></span><span style="display:flex;"><span>            spheres[i].position = agents[i].transform.position;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnStateChanged(CullingGroupEvent evt)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        agents[evt.index].ApplyBudget(evt.isVisible, evt.currentDistance);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnDisable()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">group</span> == <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">group</span>.onStateChanged = <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">group</span>.Dispose();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">group</span> = <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>There are a few details worth keeping.</p>
<p><code>SetBoundingSpheres</code> does not copy the array. The group retains a reference to it. Updating an existing sphere&rsquo;s position therefore does not require calling <code>SetBoundingSpheres</code> again. This is both convenient and a reason not to replace or lose the array casually.</p>
<p><code>SetBoundingSphereCount</code> tells Unity how much of the allocated array is active. The backing array can be larger than the current count. Unity recommends reserving enough capacity to avoid resizing at runtime, which is valuable for crowds and pooled objects.</p>
<p>The distance reference can be a <code>Transform</code>. That makes a player or camera rig a sensible source without manually pushing a new vector every frame. One group supports one reference point and one camera.</p>
<p>Finally, the group implements <code>IDisposable</code>. Creating it in <code>OnEnable</code> and disposing it in <code>OnDisable</code> gives the native resource an explicit lifetime. Skipping disposal is not an acceptable shortcut.</p>
<h2 id="keep-the-receiver-boring">Keep the receiver boring</h2>
<p>The receiver should apply a policy, not rediscover visibility. Here is a deliberately small example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CullableAgent</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> MonoBehaviour expensiveAi;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> Animator animator;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> ParticleSystem cosmeticEffect;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> ApplyBudget(<span style="color:#66d9ef">bool</span> cameraVisible, <span style="color:#66d9ef">int</span> distanceBand)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Close and medium agents can think; farther agents use a cheaper world model.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (expensiveAi != <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>            expensiveAi.enabled = distanceBand &lt;= <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Presentation only matters when the camera can see it.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (animator != <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>            animator.enabled = cameraVisible &amp;&amp; distanceBand &lt;= <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (cosmeticEffect == <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">bool</span> shouldPlay = cameraVisible &amp;&amp; distanceBand == <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (shouldPlay &amp;&amp; !cosmeticEffect.isPlaying)
</span></span><span style="display:flex;"><span>            cosmeticEffect.Play();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> (!shouldPlay &amp;&amp; cosmeticEffect.isPlaying)
</span></span><span style="display:flex;"><span>            cosmeticEffect.Stop(<span style="color:#66d9ef">true</span>, ParticleSystemStopBehavior.StopEmitting);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is an example policy, not a universal agent controller. Disabling an <code>Animator</code> can leave a character frozen in its current pose. That may be fine for an invisible agent but wrong when it becomes visible again, especially if root motion owns the transform. Disabling an AI behaviour may also discard timing assumptions.</p>
<p>Production code often replaces the booleans with explicit tiers such as <code>Full</code>, <code>Reduced</code>, <code>PresentationOnly</code>, and <code>Dormant</code>. Each subsystem can then decide what a tier means. The culling controller reports state; it does not become the owner of animation, AI, networking, and effects.</p>
<h2 id="callbacks-are-delayed-state-not-an-immediate-physics-query">Callbacks are delayed state, not an immediate physics query</h2>
<p>One of the easiest mistakes is to move a sphere and immediately ask the group for its new result.</p>
<p>Culling data is updated when Unity performs culling for the target camera. Until that happens, <code>IsVisible</code>, <code>GetDistance</code>, callbacks, and bulk query results describe the latest completed culling pass. Unity&rsquo;s manual recommends treating the API asynchronously for this reason.</p>
<p>That has two consequences.</p>
<p>First, do not use <code>CullingGroup</code> for correctness decisions that require an answer at an exact instant. A melee hit test, line-of-sight shot, or security check needs physics, geometry, or authoritative game state. Camera culling is not a substitute.</p>
<p>Second, callbacks should tolerate a frame of delay. Simulation budgeting, effect control, crowd detail, and spawn-site filtering usually can. If a one-frame delay breaks the feature, the feature probably needs a different query.</p>
<p>The <code>onStateChanged</code> callback is efficient because it only reports transitions. It receives a <code>CullingGroupEvent</code> containing the sphere index, previous and current distance bands, visibility, and convenience flags such as <code>hasBecomeVisible</code> and <code>hasBecomeInvisible</code>.</p>
<p>Use those transition fields when setup and teardown differ:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnStateChanged(CullingGroupEvent evt)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (evt.hasBecomeVisible)
</span></span><span style="display:flex;"><span>        agents[evt.index].PreparePresentation();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (evt.hasBecomeInvisible)
</span></span><span style="display:flex;"><span>        agents[evt.index].ReleasePresentation();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (evt.previousDistance != evt.currentDistance)
</span></span><span style="display:flex;"><span>        agents[evt.index].SetSimulationBand(evt.currentDistance);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Heavy work should still be deferred. Loading an asset, instantiating a complex prefab, or rebuilding navigation state directly inside a burst of callbacks can merely move the hitch. Queue expensive transitions and process a limited number per frame.</p>
<h2 id="queries-are-useful-when-transitions-are-not-enough">Queries are useful when transitions are not enough</h2>
<p>Callbacks are the normal choice for persistent state. Sometimes a system needs a snapshot instead.</p>
<p>A spawn director may ask for currently hidden candidates. A debug overlay may need every sphere in a particular distance band. A streaming controller may periodically reconcile its active set.</p>
<p>For those cases, the API provides <code>IsVisible(index)</code>, <code>GetDistance(index)</code>, and several <code>QueryIndices</code> overloads. A bulk query fills a caller-provided integer array with matching sphere indices.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">int</span>[] hiddenCandidates = <span style="color:#66d9ef">new</span> <span style="color:#66d9ef">int</span>[<span style="color:#ae81ff">256</span>];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> FindHiddenCandidates(CullingGroup <span style="color:#66d9ef">group</span>)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">group</span>.QueryIndices(<span style="color:#66d9ef">false</span>, hiddenCandidates, <span style="color:#ae81ff">0</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The result buffer sets the maximum returned by that call. If more matches exist than the buffer can hold, continue from a later starting index or use a larger reusable buffer. Do not allocate a fresh array every time a spawn decision runs; that would turn a culling optimization into managed allocation churn.</p>
<p>Queries still return the last completed camera-culling results. They do not force an update.</p>
<h2 id="dynamic-sets-need-stable-ownership-of-indices">Dynamic sets need stable ownership of indices</h2>
<p>A fixed inspector array is enough for the first implementation. Pooled enemies, procedural props, and streaming worlds need registration and removal.</p>
<p>The sphere index becomes the identity passed through every event, so there must be a matching index-to-object table. A compact array or list works well. On removal, swap the final active entry into the removed slot, update both the sphere and object entry, reduce the active count, and call <code>SetBoundingSphereCount</code>.</p>
<p>That avoids shifting a large tail of elements:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>remove index 3
</span></span><span style="display:flex;"><span>move last active sphere and owner into index 3
</span></span><span style="display:flex;"><span>decrement active count
</span></span><span style="display:flex;"><span>update SetBoundingSphereCount(activeCount)
</span></span></code></pre></div><p>If outside code stores culling indices, the swap must also update that object&rsquo;s stored handle. Better yet, keep indices private to the manager and expose an opaque registration handle.</p>
<p>There is one awkward edge case: a state-change event may already be pending for an index that is being recycled. A generation counter on each slot can prevent stale work from being applied to a newly registered object. Smaller projects can avoid that machinery by batching registration changes at a known point in the frame, after processing culling events.</p>
<p>Do not build a general-purpose handle allocator before the project needs dynamic membership. Start with a fixed set, measure the result, and add slot management when pooling or streaming makes it necessary.</p>
<h2 id="one-camera-and-one-reference-point-are-real-constraints">One camera and one reference point are real constraints</h2>
<p>A single <code>CullingGroup</code> evaluates one camera and one distance reference point. Split-screen, security cameras, minimap cameras, scene views, and spectator cameras complicate the policy.</p>
<p>Unity recommends using one group per camera when visibility must be evaluated from multiple cameras. Those groups can share the same bounding-sphere array because the array is referenced rather than copied. The game must then combine results, usually treating an object as visible if any relevant gameplay camera sees it.</p>
<p>Be careful about cameras that do not represent a player. A minimap camera should not necessarily wake full animation or AI across the whole map. Define which cameras participate in which budget decisions.</p>
<p>Distance has the same issue. A local co-op game may need the minimum band across two players, but one group only accepts one reference. Multiple groups or a separate distance system may be clearer.</p>
<p>At that point, a spatial hash, quadtree, ECS-based solution, or jobified distance pass can become the better tool. <code>CullingGroup</code> is valuable because it is narrow. Trying to hide its constraints behind a sprawling framework removes much of that value.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<h3 id="treating-visibility-as-line-of-sight">Treating visibility as line of sight</h3>
<p>Frustum and static occlusion results are not gameplay perception. Dynamic occluders are ignored, and an enclosing sphere is only an approximation. Use physics or a purpose-built visibility system when an AI must know whether it can actually see a target.</p>
<h3 id="deactivating-the-whole-gameobject">Deactivating the whole GameObject</h3>
<p><code>SetActive(false)</code> can stop scripts needed to wake the object, interrupt coroutines, alter pooling lifecycle, and disable colliders that remain important to gameplay. Disable or reduce expensive subsystems deliberately. If a fully dormant object must disappear, keep its lightweight record in a manager that remains active.</p>
<h3 id="updating-every-policy-every-frame">Updating every policy every frame</h3>
<p>The point of <code>onStateChanged</code> is to react to transitions. Reapplying the same animation, AI, and particle settings each frame adds work and can restart systems accidentally.</p>
<h3 id="allocating-exact-size-arrays-repeatedly">Allocating exact-size arrays repeatedly</h3>
<p>Reserve sphere capacity and reuse query result buffers. <code>SetBoundingSphereCount</code> exists so array capacity and active count can differ.</p>
<h3 id="using-tiny-distance-thresholds-without-hysteresis">Using tiny distance thresholds without hysteresis</h3>
<p>An object hovering around a threshold can bounce between bands. <code>CullingGroup</code> does not turn a noisy policy into a stable one. Choose bands with meaningful spacing, and add delayed promotion or hysteresis in the receiving policy when transitions trigger expensive work.</p>
<h3 id="assuming-the-callback-is-free">Assuming the callback is free</h3>
<p>The visibility calculation may be grouped, but callback code still runs. A thousand objects crossing a threshold together can create a spike if each callback starts effects, allocates data, or loads content. Budget the transitions as carefully as the steady state.</p>
<h2 id="measure-the-result-that-matters">Measure the result that matters</h2>
<p>The success metric is not “fewer active GameObjects.” It is frame time and frame-time stability on target hardware.</p>
<p>Profile before and after with a representative scene. Look at script update time, animation, particles, rendering, and garbage collection. Test a camera cut or teleport that makes many spheres change state at once. Walk back and forth across distance thresholds. Verify split-screen and secondary-camera behaviour if they exist.</p>
<p>Also test correctness:</p>
<ul>
<li>Can off-screen AI still affect gameplay when required?</li>
<li>Do agents resume in a valid state?</li>
<li>Does an effect restart cleanly?</li>
<li>Can an inactive spawn point become eligible?</li>
<li>Are networked objects still simulated by the authority?</li>
<li>Does a camera teleport produce a transition storm?</li>
</ul>
<p>A culling policy that saves a millisecond but causes guards to forget the player is not an optimization. It is a gameplay bug with a good profiler screenshot.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Use <code>CullingGroup</code> when many objects need the same coarse answer: whether a camera can potentially see them and which distance tier they occupy.</p>
<p>Keep one owner for the group, its sphere array, and the index-to-object mapping. Reserve capacity, update sphere positions without replacing the array, react to state changes instead of polling every object, and dispose the group with its owner. Let distance choose simulation quality and visibility choose presentation where that split matches the game.</p>
<p>Most importantly, treat the result as a budget signal rather than truth. It is not line of sight, physics, networking authority, or exact distance. It is a cheap way to stop spending full price on objects the player cannot currently notice.</p>
<p>That narrow job is enough to make it useful.</p>
<h2 id="references">References</h2>
<ul>
<li>Unity Manual: <a href="https://docs.unity3d.com/6000.0/Documentation/Manual/CullingGroupAPI.html">Introduction to the CullingGroup API</a></li>
<li>Unity Manual: <a href="https://docs.unity3d.com/6000.0/Documentation/Manual/CullingGroupAPI-getstarted.html">Create a Culling Group</a></li>
<li>Unity Manual: <a href="https://docs.unity3d.com/6000.0/Documentation/Manual/CullingGroupAPI-get-culling-results.html">Get culling results</a></li>
<li>Unity Scripting API: <a href="https://docs.unity3d.com/6000.0/Documentation/ScriptReference/CullingGroup.html">CullingGroup</a></li>
<li>Unity Scripting API: <a href="https://docs.unity3d.com/6000.0/Documentation/ScriptReference/CullingGroupEvent.html">CullingGroupEvent</a></li>
<li>Unity Scripting API: <a href="https://docs.unity3d.com/6000.0/Documentation/ScriptReference/CullingGroup.QueryIndices.html">CullingGroup.QueryIndices</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Unity Core CLI: 5 features I&#39;m excited for</title>
            <link>https://blog.jonmikeys.com/posts/unity-core-cli-5-features-im-excited-for/</link>
            <pubDate>Thu, 10 Sep 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/unity-core-cli-5-features-im-excited-for/</guid>
            <description>A practical look at five Unity CLI features that could make editor installs, CI builds, tests, version control, and connected Editor automation less brittle.</description>
            <content type="html"><![CDATA[<p>Unity projects already have a command line, but for a long time it has not felt like a product-shaped command line. Most teams that automate Unity eventually learn the older pattern: invoke the Editor executable directly, pass a pile of batchmode flags, call a static method inside the project, capture a log file, and hope the failure mode is easy to parse. It works, but it often feels like building a small adapter around the Editor rather than using a tool designed for automation.</p>
<p>The newer Unity CLI points at a better shape. The official docs describe it as “a standalone command-line tool for installing and managing Unity Editors and modules from a terminal.” That matters because the tool is separate from the desktop Hub experience, and because it starts to put common Unity workflow chores behind named commands: install an Editor, add modules, open a project, run tests, build a player, inspect the machine, and talk to a connected Editor.</p>
<p>One naming caveat belongs up front. Unity&rsquo;s current documentation calls the tool <strong>Unity CLI</strong>, not “Unity Core CLI.” That title is still useful as a human label for the kind of thing Unity developers have wanted: a core terminal surface for daily Unity work. It should not be confused with the Unity Gaming Services CLI, and it is not the same thing as the older Unity Editor command-line argument page. The tool is also marked experimental in the docs, so exact syntax should be checked against <code>unity --help</code> on the installed version before turning any of this into production automation.</p>
<p><img src="/images/posts/unity-core-cli-5-features-im-excited-for/diagram.svg" alt="Five Unity CLI workflow lanes"></p>
<p>With that caveat in place, there are five areas that look genuinely useful for Unity teams.</p>
<h2 id="1-scriptable-editor-and-module-installation">1. Scriptable Editor and module installation</h2>
<p>The first feature is the least glamorous, which is usually why it matters. Installing the right Unity Editor version on a fresh machine should not be a ceremony. A developer workstation, a CI runner, a build VM, and a temporary test box all need the same basic ingredients: a specific Editor version and the modules required by the target platforms.</p>
<p>Historically, that often meant using Unity Hub manually, scripting around Hub, or baking large machine images that go stale. The Unity CLI gives that workflow a cleaner terminal surface:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unity install lts
</span></span><span style="display:flex;"><span>unity install 6000.3.7f1
</span></span><span style="display:flex;"><span>unity install 6000.3.7f1 -m android --cm
</span></span><span style="display:flex;"><span>unity install lts -m ios android webgl
</span></span><span style="display:flex;"><span>unity install-modules -e 6000.3.7f1 -m ios android
</span></span><span style="display:flex;"><span>unity editors -i
</span></span></code></pre></div><p>There are a few details in that small command set that are easy to understate. Version aliases such as <code>lts</code>, <code>latest</code>, and major/minor streams make scripts less tied to a hardcoded patch version when that is what the workflow wants. Exact versions still exist for reproducible agents and release branches. Modules can be added at install time or later with <code>install-modules</code>. The <code>--cm</code> or <code>--childModules</code> option is there for cases such as Android, where the module is not useful unless the SDK and NDK pieces come along for the ride.</p>
<p>That is a better division of labor. The build script should declare what it needs. The machine should be able to become that machine from a terminal. The person reading the script should not have to know which Hub screen would have been clicked.</p>
<p>For teams that maintain multiple Unity versions, <code>unity editors -i</code> and <code>unity editors default [version]</code> are also useful because they make installed Editor state visible. A broken CI machine often starts with something boring: the wrong Editor version, a missing platform module, or a default Editor pointing at an older install. The sooner that state can be listed and checked by a script, the less time gets wasted reading a 20,000-line Unity log.</p>
<p>The best version of this feature is not a fancy one-click setup. It is a boring bootstrap script that a new runner can run without a human sitting beside it. Unity projects have enough moving parts already. Editor installation should not be one of the mysterious ones.</p>
<h2 id="2-build-and-test-commands-that-read-like-the-job">2. Build and test commands that read like the job</h2>
<p>The second feature is the one most CI setups will notice first. Unity automation usually ends up with a wrapper script named after the thing the team wanted in the first place: build, test, smoke test, export Android, run Edit Mode tests. The wrapper calls Unity in batchmode, invokes a project method, and maps the result back into whatever CI system is watching.</p>
<p>A CLI command named <code>unity build</code> or <code>unity test</code> is a cleaner starting point:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unity open ./MyProject
</span></span><span style="display:flex;"><span>unity build
</span></span><span style="display:flex;"><span>unity test
</span></span><span style="display:flex;"><span>unity run
</span></span></code></pre></div><p>A named command does not remove the need to understand Unity&rsquo;s build pipeline. It does remove some of the accidental complexity around reaching it. When the CLI owns the verb, it can own the boring behavior that every team otherwise has to rediscover: consistent flags, predictable output, better exit codes, and CI-friendly defaults.</p>
<p>The build provenance work is the part worth watching. Unity&rsquo;s release notes describe a provenance manifest written next to build output, recording the Editor version, resolved package set, build target, Git revision, and timestamps. The notes also say the manifest is written for failed builds and leaves out paths, credentials, and machine-identifying details.</p>
<p>That is exactly the kind of artifact a build should leave behind. A failed build without provenance is a mystery. A passed build without provenance is only slightly better, because the team still has to answer what was actually built. The useful question is not only “did it pass?” The useful question is “what ingredients produced this result?”</p>
<p>Test support is also moving in the right direction. The documented and release-note surface includes <code>unity test</code>, NUnit report output, sharding with <code>--shard N/M</code>, retries with <code>--retries N</code>, and failed-test reruns with <code>--rerun-failed</code>. Those features sound mundane until a project has thousands of Play Mode and Edit Mode tests fighting for time on shared runners.</p>
<p>Sharding matters when a single test job becomes the longest pole in the pipeline. Retries matter when the team is separating real failures from a small number of flaky tests. Rerunning failed tests matters because it can produce a tighter signal than rerunning everything. None of those features make bad tests good. They do give the pipeline a more practical set of controls.</p>
<p>Exit codes are part of the story too. The reference docs distinguish test failures from other operation failures. Exit code <code>8</code> is used when tests ran and one or more failed; exit code <code>6</code> covers cases where the primary operation failed or a build/test run did not finish. That distinction lets CI report a failed test suite differently from a broken runner, missing license, or crashed Editor. A small difference, but small differences are what make automation trustworthy.</p>
<h2 id="3-output-formats-and-diagnostics-that-belong-in-ci">3. Output formats and diagnostics that belong in CI</h2>
<p>Unity logs are useful, but they are not an API. A human can read them. A script can grep them. Neither of those is the same as a command-line tool promising machine-readable output.</p>
<p>The Unity CLI docs list several output formats:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unity editors -i --format json
</span></span><span style="display:flex;"><span>unity editors -i --format tsv
</span></span><span style="display:flex;"><span>unity install lts --format ndjson
</span></span><span style="display:flex;"><span>unity install lts --format github
</span></span></code></pre></div><p>The shape matters. <code>human</code> output is for terminals. <code>tsv</code> is useful when output is piped or saved. <code>json</code> gives a structured envelope with fields such as <code>success</code>, <code>command</code>, <code>data</code>, <code>errors</code>, and <code>warnings</code>. <code>ndjson</code> works better for long-running commands because progress can be emitted as a stream of JSON objects. <code>github</code> maps messages into GitHub Actions annotations like <code>::error::</code> and <code>::warning::</code>.</p>
<p>That sounds like normal CLI behavior because it is. Unity getting closer to normal CLI behavior is the point.</p>
<p>A good CI command should be quiet when asked, structured when piped, and explicit when it fails. The docs also call out environment variables such as <code>UNITY_FORMAT</code>, <code>UNITY_QUIET</code>, <code>UNITY_NO_BANNER</code>, <code>UNITY_NON_INTERACTIVE</code>, <code>UNITY_PROJECT_PATH</code>, and <code>UNITY_BUILD_TIMEOUT</code>. Those knobs are useful because CI configuration often wants policy outside the command itself. A job can set <code>UNITY_NON_INTERACTIVE</code> once, then every Unity CLI call in that job inherits the rule.</p>
<p>The diagnostic commands are the other half:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unity doctor
</span></span><span style="display:flex;"><span>unity doctor --ci
</span></span><span style="display:flex;"><span>unity cache key
</span></span><span style="display:flex;"><span>unity env
</span></span><span style="display:flex;"><span>unity logs
</span></span><span style="display:flex;"><span>unity diagnose
</span></span></code></pre></div><p><code>unity doctor --ci</code> is the standout. The release notes describe it as checking whether a machine can finish a build or test run, including license, required Editor, disk space, and network reachability. That is the kind of command a pipeline should run before spending twenty minutes starting a build that had no chance of completing.</p>
<p><code>unity cache key</code> is another practical detail. A Unity <code>Library</code> cache is only useful when the key matches the things that make the cache valid. The release notes say the key is derived from the Editor version, resolved package set, and build target. That is the right neighborhood. A cache key based only on branch name or commit hash tends to be either too broad or too narrow. A Unity-aware key gives the CI system a better chance of avoiding stale imports without throwing away every cache.</p>
<p>The exit-code table also helps automation. A retryable Unity service failure can be treated differently from a usage error. A configuration-required error can point the maintainer toward setup instead of source changes. A SIGTERM from the runner should not be reported as a Unity test failure. These distinctions are boring until the night a release build fails and the only message is “process exited with code 1.”</p>
<h2 id="4-version-control-commands-that-understand-unity-projects">4. Version-control commands that understand Unity projects</h2>
<p>The version-control commands may be the most Unity-specific part of the CLI. Generic Git can tell a team which files changed. It cannot always explain what those changes mean inside a Unity project.</p>
<p>Unity projects carry their own version-control problems: <code>.meta</code> files, GUID pairing, scene and prefab YAML, Git LFS for binary assets, UnityYAMLMerge setup, branch switching that triggers expensive reimports, and pull requests where the meaningful change is hidden behind file IDs.</p>
<p>The CLI&rsquo;s <code>vcs</code> surface points directly at those pain points:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unity vcs status
</span></span><span style="display:flex;"><span>unity vcs doctor --fix
</span></span><span style="display:flex;"><span>unity vcs merge-setup
</span></span><span style="display:flex;"><span>unity vcs diff Assets/SomePrefab.prefab
</span></span><span style="display:flex;"><span>unity vcs affected
</span></span><span style="display:flex;"><span>unity vcs switch feature/some-branch
</span></span><span style="display:flex;"><span>unity vcs git migrate-lfs
</span></span></code></pre></div><p>This is where a Unity-aware tool can beat a generic script. <code>unity vcs status</code> can group changes by Unity meaning and identify <code>.meta</code> pairing problems. A missing or mismatched <code>.meta</code> file is not just another changed file; it can break references across the project. Catching that early is worth more than a prettier status view.</p>
<p><code>unity vcs merge-setup</code> is another useful example. Scene and prefab merges are painful enough without each developer configuring the merge tool differently. A one-command setup for UnityYAMLMerge lowers the odds that two machines treat the same conflict in two different ways.</p>
<p>The <code>diff</code> command is the one that sounds most useful for code review. A normal prefab diff often exposes <code>fileID</code> noise and YAML structure before it exposes the thing a reviewer cares about. The release notes describe <code>unity vcs diff &lt;path&gt;</code> as reading scene and prefab changes by GameObject and component name instead. That is a much better review unit. A reviewer wants to know which component changed, not decode serialized object trivia by hand.</p>
<p><code>unity vcs affected</code> could become useful in larger projects. If a change can report which prefabs, scenes, assets, or tests are impacted, teams can make better choices about targeted validation. It will not replace full test runs before release, and it should not become an excuse to skip broad checks. But as a local or pull-request signal, “what did this change probably touch?” is a question Unity is better positioned to answer than plain Git.</p>
<p>There is also practical value in <code>unity vcs switch &lt;branch&gt;</code> reporting expected reimport cost before switching. Anyone who has bounced between large Unity branches knows the pain: a small code review can turn into an import penalty because asset state changed under the project. A warning does not remove the cost, but it lets the developer make an informed choice instead of discovering it after the switch.</p>
<p>This section deserves the largest caveat. Much of this VCS surface appears in recent beta release notes, and the whole CLI is experimental. It should be treated as promising, not as a settled contract. Still, it is the right kind of promise: Unity-specific automation for Unity-specific version-control problems.</p>
<h2 id="5-connected-editor-automation-through-pipeline-commands-eval-and-mcp">5. Connected Editor automation through Pipeline, commands, eval, and MCP</h2>
<p>The fifth feature points beyond CI. Installing Editors and running builds are obvious automation targets. The more interesting daily workflow is what happens when the CLI can talk to a running Editor.</p>
<p>Unity&rsquo;s Pipeline package is described as a way to “Control the Unity Editor remotely through a local HTTP API to automate builds, tests, and development workflows.” The documented flow starts with authentication, then installing and listing the Pipeline package:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unity auth login
</span></span><span style="display:flex;"><span>unity pipeline install
</span></span><span style="display:flex;"><span>unity pipeline list
</span></span></code></pre></div><p>Once the Editor is connected, the CLI surface includes commands such as:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unity status
</span></span><span style="display:flex;"><span>unity list
</span></span><span style="display:flex;"><span>unity command
</span></span><span style="display:flex;"><span>unity command &lt;name&gt;
</span></span><span style="display:flex;"><span>unity command &lt;name&gt; --detach
</span></span><span style="display:flex;"><span>unity eval &lt;expr&gt;
</span></span><span style="display:flex;"><span>unity job status
</span></span><span style="display:flex;"><span>unity job wait
</span></span><span style="display:flex;"><span>unity job cancel
</span></span></code></pre></div><p>That shape is interesting because it gives tooling a path into the Editor without pretending the Editor is not there. Some Unity work belongs inside the Editor process. Asset database operations, project-specific menu commands, certain validation steps, and custom tools often need the actual project context. The older answer was usually another <code>-executeMethod</code> wrapper or an editor script wired into a menu item. A connected command surface could make those jobs easier to discover and easier to run.</p>
<p><code>unity list</code> is a small command with a big implication: list every tool a connected Editor registers, with parameter schema. That gives local tools and AI agents a way to ask “what can this project do?” instead of relying on tribal knowledge. It also nudges teams toward named, registered operations instead of one-off scripts hidden in a folder.</p>
<p>The MCP angle is worth mentioning carefully. Unity&rsquo;s docs say the MCP server included in the in-Editor AI assistant package has been deprecated, and that the <code>unity mcp</code> command in Unity CLI replaces it. They also say <code>unity command</code> and <code>unity eval</code> drive the Editor directly without MCP, and that those direct commands are faster and use fewer tokens than MCP for that job.</p>
<p>That is the right framing for most developers. MCP is useful when an agent needs a tool protocol. Direct CLI commands are better when a script already knows what it wants. The exciting part is not “AI can now touch Unity.” The exciting part is that Unity appears to be defining a clearer local automation boundary: install the Pipeline package, expose commands from the Editor, call them from a terminal, and manage detached jobs when the work takes time.</p>
<p>For teams building custom validation tools, content checks, import audits, or one-button release chores, this could be the difference between a tool that only works from one developer&rsquo;s menu bar and a tool that works from a terminal, a CI job, or an agent with the same command name.</p>
<h2 id="a-measured-caveat-experimental-means-verify-before-depending-on-it">A measured caveat: experimental means verify before depending on it</h2>
<p>The reason to be excited is not that every command listed above should be dropped into production today. The official docs mark Unity CLI as experimental, and several of the most interesting pieces are described in beta release notes. That means two habits are mandatory.</p>
<p>First, check the installed CLI, not only the web docs:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unity --help
</span></span><span style="display:flex;"><span>unity build --help
</span></span><span style="display:flex;"><span>unity test --help
</span></span><span style="display:flex;"><span>unity vcs --help
</span></span></code></pre></div><p>The docs themselves point developers toward <code>unity --help</code> as the source that matches the installed version. That matters for beta tooling. A blog post, CI template, or team wiki can go stale faster than the command line installed on the runner.</p>
<p>Second, introduce the CLI at the edges before making it the foundation of the whole pipeline. Editor installation on a disposable runner is a good early target. <code>unity doctor --ci</code> before an existing build is another. Structured output for one inspection command is low risk. Replacing every build, test, VCS, and Editor automation script at once is how a promising tool becomes tomorrow&rsquo;s outage.</p>
<p>There is also a naming risk. “Unity Core CLI” is not the official documentation label found during research. The article title can keep that phrase because it describes the idea people are reacting to: a central command-line layer for Unity work. The body should use Unity&rsquo;s own name, Unity CLI, so readers can find the docs and avoid mixing it up with Unity Gaming Services CLI or older Editor arguments.</p>
<p>The useful posture is cautious optimism. Unity teams already script these jobs. They already install Editors on runners, run tests in batchmode, parse logs, configure YAML merge, manage <code>Library</code> caches, and call custom Editor commands. The Unity CLI is exciting because it names those jobs directly. If it keeps moving in this direction, the terminal can become less of a workaround and more of a supported part of the Unity workflow.</p>
<h2 id="references">References</h2>
<ul>
<li>Unity CLI overview: <a href="https://docs.unity.com/en-us/unity-cli/unity-cli.md">https://docs.unity.com/en-us/unity-cli/unity-cli.md</a></li>
<li>Install and use Unity CLI: <a href="https://docs.unity.com/en-us/unity-cli/use-unity-cli.md">https://docs.unity.com/en-us/unity-cli/use-unity-cli.md</a></li>
<li>Unity CLI command reference: <a href="https://docs.unity.com/en-us/unity-cli/unity-cli-reference.md">https://docs.unity.com/en-us/unity-cli/unity-cli-reference.md</a></li>
<li>Unity CLI release notes: <a href="https://docs.unity.com/en-us/unity-cli/release-notes.md">https://docs.unity.com/en-us/unity-cli/release-notes.md</a></li>
<li>Unity Pipeline package: <a href="https://docs.unity.com/en-us/unity-production-pipeline/local-tools-cli/unity-pipeline-package.md">https://docs.unity.com/en-us/unity-production-pipeline/local-tools-cli/unity-pipeline-package.md</a></li>
<li>Replacing the deprecated in-Editor MCP server with Unity CLI: <a href="https://docs.unity.com/en-us/unity-cli/replace-mcp-server-unity-cli.md">https://docs.unity.com/en-us/unity-cli/replace-mcp-server-unity-cli.md</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>C# Records in Unity: Value Semantics Without Fighting the Engine</title>
            <link>https://blog.jonmikeys.com/posts/csharp-records-in-unity/</link>
            <pubDate>Wed, 09 Sep 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/csharp-records-in-unity/</guid>
            <description>Where C# records fit in Unity projects, where they cause trouble, and how to keep serialized data, runtime values, and live game objects separate.</description>
            <content type="html"><![CDATA[<p>C# records are useful when a type is mostly a bundle of facts.</p>
<p>That sounds small, because it is. Records are not a new way to build every gameplay object. They are not a replacement for <code>MonoBehaviour</code>, <code>ScriptableObject</code>, serialized inspector data, mutable services, or performance-sensitive simulation state. They are a language feature for value-shaped data: data where two instances with the same contents should usually be treated as equal.</p>
<p>That narrow definition is what makes records useful in Unity. The trouble starts when the feature gets dragged outside that shape.</p>
<p>A Unity project already has several kinds of data living together. Some data is authored in the inspector. Some data moves through runtime systems as commands, events, snapshots, or requests. Some data belongs to live scene objects with identity, lifetime, references, and mutable state. A record can make the middle group cleaner. It usually makes the first and third groups worse.</p>
<p><img src="/images/posts/csharp-records-in-unity/diagram.svg" alt="Records in Unity decision sketch"></p>
<p>The practical rule is simple: use records after a boundary, not as the boundary itself. Let Unity serialize boring authoring data. Convert that data into compact runtime values when value semantics help. Keep live gameplay objects as normal objects with identity and behavior.</p>
<h2 id="what-a-record-buys-you">What a record buys you</h2>
<p>A normal C# class compares by reference unless it overrides equality. Two instances can contain the same values and still be considered different objects because they are different allocations. That is often correct. A player, enemy, projectile, UI widget, or service is not the same thing as another object just because some fields match.</p>
<p>For data messages, reference equality is often noise.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">DamageRequest</span>(
</span></span><span style="display:flex;"><span>    EntityId Source,
</span></span><span style="display:flex;"><span>    EntityId Target,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> Amount,
</span></span><span style="display:flex;"><span>    DamageType Type);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> hit = <span style="color:#66d9ef">new</span> DamageRequest(attackerId, targetId, <span style="color:#ae81ff">25</span>, DamageType.Fire);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> criticalHit = hit with { Amount = <span style="color:#ae81ff">50</span> };
</span></span></code></pre></div><p><code>DamageRequest</code> is a good record candidate because it describes a thing the game should process. It is not the attacker. It is not the target. It is not a component with a lifetime. It is a value-shaped request: source, target, amount, type.</p>
<p>A record class gives you value-based equality, generated members, a useful <code>ToString</code>, deconstruction support when you use positional syntax, and <code>with</code> expressions for copying with a changed field. Those features are handy in tests, logs, command queues, editor tools, import pipelines, and glue code between systems.</p>
<p>The generated equality is the main signal. If two requests contain the same source, target, amount, and damage type, treating them as equal probably makes sense. If that sentence sounds wrong for a type, the type probably should not be a record.</p>
<h2 id="unity-objects-are-identity-shaped">Unity objects are identity-shaped</h2>
<p>Most live Unity objects are not value-shaped.</p>
<p>A player object is not equal to another player object because both have 100 health. A projectile instance is not equal to another projectile because both came from the same prefab. A <code>MonoBehaviour</code> is a component attached to a scene object. A <code>ScriptableObject</code> is often an asset with editor identity, references, import behavior, and shared usage across scenes.</p>
<p>Those objects exist somewhere. They have identity.</p>
<p>Records describe something. That difference matters more than the syntax.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Identity-shaped                      Value-shaped
</span></span><span style="display:flex;"><span>---------------                     ----------------
</span></span><span style="display:flex;"><span>PlayerController                    DamageRequest
</span></span><span style="display:flex;"><span>Enemy MonoBehaviour                 SpawnCommand
</span></span><span style="display:flex;"><span>InventoryService                    StatSnapshot
</span></span><span style="display:flex;"><span>WeaponDefinition asset              MatchmakingOptions
</span></span><span style="display:flex;"><span>Quest ScriptableObject              SaveMigrationStep
</span></span></code></pre></div><p>The left side owns behavior, lifetime, engine references, or shared state. The right side moves facts across a boundary. Records belong mostly on the right side.</p>
<p>This is not a purity rule. Game code should not care about language-feature purity. The point is to avoid misleading defaults. A record says equality is based on contents. A scene object usually says equality is based on identity. If the type&rsquo;s meaning disagrees with the generated behavior, the generated behavior is working against the code.</p>
<h2 id="do-not-use-records-as-unity-serialized-data-by-default">Do not use records as Unity serialized data by default</h2>
<p>Unity serialization is not a general-purpose .NET serializer. It has its own supported field shapes, object-reference behavior, prefab override rules, domain reload behavior, editor display assumptions, and asset persistence model. Records were designed for C# data modeling, not for Unity&rsquo;s serialized object graph.</p>
<p>Unity&rsquo;s documentation calls out record caveats directly, including serialization concerns. The safe default is to keep records out of serialized fields unless the exact Unity version and serialization path have been tested.</p>
<p>This is fine:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">InventoryChange</span>(ItemId Item, <span style="color:#66d9ef">int</span> Delta);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">InventorySystem</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Apply(InventoryChange change)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Runtime message. Unity does not serialize this object.</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is where the risk starts:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LootTable</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> DropRule defaultRule;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">DropRule</span>(ItemId Item, <span style="color:#66d9ef">float</span> Weight);
</span></span></code></pre></div><p>The second version puts a record directly into Unity&rsquo;s serialized object graph. Now inspector display, prefab overrides, generated members, <code>init</code> accessors, field rules, and Unity version support all have to line up. Some teams can make a specific version of that work. It is still the wrong default for a shared project unless there is a clear reason and a test around it.</p>
<p>For inspector data, boring code is usually better:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[Serializable]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DropRuleData</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> ItemDefinition item;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> weight;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then convert the authoring data into a runtime value after validation:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">DropRule</span>(ItemId Item, <span style="color:#66d9ef">float</span> Weight);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> DropRule ToRuntime(DropRuleData data)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> DropRule(data.item.Id, data.weight);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That conversion is not ceremony for its own sake. It gives each side the shape it wants. Unity gets inspector-friendly fields. Runtime code gets a compact value object. Designers do not have to care how records work, and gameplay systems do not have to drag inspector concerns through every method signature.</p>
<h2 id="init-support-and-language-versions-can-still-bite"><code>init</code> support and language versions can still bite</h2>
<p>Records are tied to newer C# language features. In modern .NET, the compiler and runtime provide the pieces records expect. Unity moves differently. A Unity version can support a particular C# language level while still having caveats around generated code, runtime profiles, IDE support, and build targets.</p>
<p>Older Unity setups that use record syntax may require an <code>IsExternalInit</code> compatibility shim:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">namespace</span> System.Runtime.CompilerServices
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">internal</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">IsExternalInit</span> { }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That shim is not scandalous. It is also not a license to spread records through the project. If the project needs it, put it in one obvious file, comment why it exists, and remove it when the Unity/runtime combination no longer needs it.</p>
<p>The boring check is the important one: the project has to compile in CI, on teammate machines, and for the target platforms. A language feature that only works in one local editor setup is not saving time. It is turning a small syntax win into a build-system problem.</p>
<p>This matters even more for <code>record struct</code>. C# 10 added record structs, but Unity language-version support depends on the Unity version and project setup. A post can show the syntax. A production project has to compile it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">struct</span> StatSnapshot(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> Health,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> Armor,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">float</span> MoveSpeed);
</span></span></code></pre></div><p>If the toolchain supports it, a small readonly record struct can be a clean value type. If the toolchain does not support it, the theoretical fit does not matter. Use a normal readonly struct or a plain class instead. The code that ships wins over the code that looks current.</p>
<h2 id="record-classes-still-allocate">Record classes still allocate</h2>
<p>A <code>record class</code> is still a reference type. Creating one allocates. A <code>with</code> expression creates a copy. Equality can walk fields. If those fields are reference types or collections, the cost and meaning can be less obvious than the syntax suggests.</p>
<p>For many uses, that cost is fine. A settings object created in a menu is fine. A save migration descriptor is fine. A combat event emitted a few times per hit is probably fine. Editor tooling data is usually fine. Test expectations are fine.</p>
<p>Hot loops are different.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#75715e">// Suspicious if this runs for thousands of agents every frame.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> next = currentSteering with
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    DesiredVelocity = desiredVelocity,
</span></span><span style="display:flex;"><span>    AvoidanceWeight = avoidanceWeight
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>The line is pleasant. Pleasant syntax does not make allocation free. If this runs inside <code>Update</code> for a crowd system, bullet pattern, pathing loop, perception pass, or planner inner loop, it may create garbage that the project did not need.</p>
<p>Do not guess here. Measure. Unity&rsquo;s profiler, allocation call stacks, and a small benchmark usually answer the question faster than an argument about style. If the record sits on a cold path, keep the clarity. If it sits on a hot path, a normal struct, <code>NativeArray</code>, pooled object, or hand-written value type may be the better trade.</p>
<p>Record structs can avoid the reference allocation, but they do not make copying free. Large structs get expensive to pass around. Mutable structs are easy to misuse. A record struct with many fields, nested structs, or reference members can move the problem instead of solving it.</p>
<p>A useful rule: record classes are for clarity on cold or moderate paths. Record structs are for small values when the supported language version and copy cost make sense. Neither one is a default performance tool.</p>
<h2 id="good-unity-use-cases">Good Unity use cases</h2>
<p>Records fit best where data crosses a system boundary and should not own behavior.</p>
<p>Commands are a clean example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">SpawnEnemyCommand</span>(
</span></span><span style="display:flex;"><span>    EnemyId Enemy,
</span></span><span style="display:flex;"><span>    Vector3 Position,
</span></span><span style="display:flex;"><span>    Quaternion Rotation,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> Level);
</span></span></code></pre></div><p>A command describes something that should happen. It can be queued, logged, tested, replayed, rejected, or converted into another request. It is not the enemy instance.</p>
<p>Events are similar:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">EnemySpawnedEvent</span>(
</span></span><span style="display:flex;"><span>    EntityId Entity,
</span></span><span style="display:flex;"><span>    EnemyId Enemy,
</span></span><span style="display:flex;"><span>    Vector3 Position);
</span></span></code></pre></div><p>An event describes something that already happened. Value equality helps in tests. Generated <code>ToString</code> helps logs. The type is easy to pass through an event bus or gameplay pipeline because it is just facts.</p>
<p>Snapshots are another good fit:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">WeaponSnapshot</span>(
</span></span><span style="display:flex;"><span>    WeaponId Weapon,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> AmmoInClip,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> ReserveAmmo,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">float</span> Heat);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> overheated = snapshot with { Heat = <span style="color:#ae81ff">1.0f</span> };
</span></span></code></pre></div><p>A snapshot is not the live weapon. It is a captured view of the weapon at a point in time. That makes it useful for tests, UI projection, save previews, combat logs, and debugging tools. Copying a snapshot with one field changed can make a rule test small without building a scene.</p>
<p>Validated runtime specs can also work well:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">AbilitySpec</span>(
</span></span><span style="display:flex;"><span>    AbilityId Id,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> BaseDamage,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">float</span> CooldownSeconds,
</span></span><span style="display:flex;"><span>    DamageType DamageType);
</span></span></code></pre></div><p>The word validated matters. Let the inspector-facing data stay flexible enough for authoring. Validate and convert it once. Then give runtime code a stable value object that no longer has to ask whether a string id is empty or a referenced asset is missing.</p>
<h2 id="be-careful-with-unity-values-and-equality">Be careful with Unity values and equality</h2>
<p>Records can contain <code>Vector3</code>, <code>Quaternion</code>, <code>Color</code>, and other Unity structs. The syntax is not the issue. The issue is whether generated equality matches the way the game thinks.</p>
<p>Floating-point equality is often too strict. Two positions can be practically identical while still failing exact equality. A generated record equality check uses the equality behavior of its fields. That is useful for some data and misleading for physics-ish results, aiming solutions, steering outputs, animation blends, and sampled positions.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">AimSolution</span>(Vector3 Direction, <span style="color:#66d9ef">float</span> TimeToImpact);
</span></span></code></pre></div><p>That type is fine as a return value. It becomes dangerous if gameplay starts treating exact equality between two aim solutions as meaningful.</p>
<p>Use records to carry values. Write explicit comparison logic when tolerance matters.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">bool</span> Approximately(AimSolution a, AimSolution b)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> Vector3.Angle(a.Direction, b.Direction) &lt; <span style="color:#ae81ff">0.5f</span>
</span></span><span style="display:flex;"><span>        &amp;&amp; Mathf.Abs(a.TimeToImpact - b.TimeToImpact) &lt; <span style="color:#ae81ff">0.05f</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Unity object references need the same caution. A record that contains a <code>GameObject</code>, <code>Transform</code>, or <code>ScriptableObject</code> mixes value equality with engine identity. Sometimes that is acceptable for a narrow request object. Often it is a smell. Passing ids, asset keys, or definition handles across a boundary usually keeps the record cleaner.</p>
<h2 id="records-are-better-than-repeated-tuples-at-api-boundaries">Records are better than repeated tuples at API boundaries</h2>
<p>Tuples are useful inside a method:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>(Vector3 position, <span style="color:#66d9ef">float</span> radius) query = (transform.position, <span style="color:#ae81ff">8f</span>);
</span></span></code></pre></div><p>That local tuple is harmless. It groups two values for a small piece of code and then disappears.</p>
<p>The same tuple shape becomes harder to defend when it crosses a boundary:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">AwarenessQuery</span>(Vector3 Position, <span style="color:#66d9ef">float</span> Radius);
</span></span></code></pre></div><p>A named record carries intent. It can be searched. It can be documented. It can grow carefully. Logs show a real type name instead of an anonymous pair. Tests can compare expected values without unpacking every field.</p>
<p>The rule is boring and useful:</p>
<ul>
<li>use tuples for throwaway grouping inside one small scope</li>
<li>use records for named data that crosses method, system, queue, or test boundaries</li>
<li>use normal classes or structs when behavior, lifecycle, serialization, or performance dominates</li>
</ul>
<p>That avoids both over-typing and under-typing. Not every pair deserves a new type. Not every repeated tuple should stay anonymous after it becomes part of the design.</p>
<h2 id="a-boundary-pattern-that-holds-up">A boundary pattern that holds up</h2>
<p>The safest Unity pattern is to split authoring data, runtime value data, and live state.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[Serializable]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">AbilityAuthoringData</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">string</span> id;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> baseDamage;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> cooldownSeconds;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> DamageType damageType;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">AbilitySpec</span>(
</span></span><span style="display:flex;"><span>    AbilityId Id,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> BaseDamage,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">float</span> CooldownSeconds,
</span></span><span style="display:flex;"><span>    DamageType DamageType);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">AbilityRuntime</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> AbilitySpec spec;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">float</span> cooldownRemaining;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> AbilityRuntime(AbilitySpec spec)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.spec = spec;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">bool</span> CanCast =&gt; cooldownRemaining &lt;= <span style="color:#ae81ff">0f</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The authoring class exists for Unity and designers. It can expose fields because the inspector wants fields. The record exists after validation. It gives runtime systems a compact, comparable value. The runtime class owns mutation, behavior, cooldowns, and lifecycle.</p>
<p>Trying to make one type serve all three roles usually creates the bug farm. If the type is inspector-friendly, it may be too mutable and too Unity-shaped for clean runtime value semantics. If it is record-shaped, it may be awkward or unsupported in Unity serialization. If it owns gameplay behavior, generated value equality probably lies about what the object means.</p>
<p>The boundary looks slightly more verbose at first. It usually removes weirdness later. Serialization bugs stay near authoring data. Runtime code gets clean values. Live objects keep identity. Tests get smaller because they can construct records without scene setup.</p>
<h2 id="when-records-are-not-worth-it">When records are not worth it</h2>
<p>Avoid records when the generated behavior does not match the model.</p>
<p>Do not use records for most <code>MonoBehaviour</code> types. Do not use them for most <code>ScriptableObject</code> assets. Avoid them for serialized inspector models unless that path has been tested and documented. Avoid them for large mutable state objects. Avoid them for entities where identity matters more than field equality. Avoid them in hot loops until profiling says the allocations or copies are harmless.</p>
<p>Also avoid records when a plain class says the truth better:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Health</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> Current { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> Max { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Health(<span style="color:#66d9ef">int</span> max)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Max = max;
</span></span><span style="display:flex;"><span>        Current = max;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Damage(<span style="color:#66d9ef">int</span> amount)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Current = Math.Max(<span style="color:#ae81ff">0</span>, Current - amount);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>Health</code> has invariants and behavior. It protects its state. It changes over time. Turning that into a record would make the code look modern while making the model less honest.</p>
<p>Good Unity code is allowed to be boring. A boring serializable class, a boring mutable runtime object, and a boring record used at the right boundary are all better than a clever type that fights its job.</p>
<h2 id="decision-checklist">Decision checklist</h2>
<p>Before adding a record to a Unity project, ask:</p>
<ol>
<li>Is the type mostly data?</li>
<li>Does value equality match the meaning of the type?</li>
<li>Will it stay out of Unity serialized fields?</li>
<li>Is allocation or copy cost acceptable for the path where it is created?</li>
<li>Does it avoid live Unity object references where ids or definitions would be clearer?</li>
<li>Does the whole team and build pipeline support the required C# language version?</li>
</ol>
<p>If most answers are yes, a record is probably reasonable.</p>
<p>If the serialization, identity, language-version, or performance answers are shaky, use a normal class, struct, or serializable data class. That is not old-fashioned. That is matching the type to the job.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Use records in Unity for value-shaped runtime data: commands, events, snapshots, validated specs, editor-tool requests, save-pipeline steps, and test expectations. Keep them away from Unity serialization by default. Treat record structs as small value-type tools only when the Unity version and build pipeline support the syntax.</p>
<p>The clean pattern is a boundary. Boring serializable Unity data on one side. Compact runtime records after validation. Normal mutable classes for live gameplay objects. That gives a Unity project the useful part of modern C# without asking the engine serializer or object model to pretend it is something else.</p>
<h2 id="references">References</h2>
<ul>
<li>Unity Manual: C# compiler and language version reference — record support caveats, Unity language version, and serialization warning.</li>
<li>Microsoft Learn: Records in C# — generated members, value equality, record classes, and record structs.</li>
<li>Microsoft Learn: The history of C# — C# 10 feature list including record structs.</li>
<li>Microsoft Learn: Tuple types — useful contrast between anonymous grouped values and named record types.</li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Build a Simple DI Container in C#: Object Graphs Without the Magic Trick</title>
            <link>https://blog.jonmikeys.com/posts/build-a-simple-di-container-in-csharp/</link>
            <pubDate>Sat, 05 Sep 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/build-a-simple-di-container-in-csharp/</guid>
            <description>A practical from-scratch walkthrough for intermediate C# and Unity developers: build a small dependency injection container, then understand registration, recursive resolution, lifetimes, circular dependency errors, and Unity integration.</description>
            <content type="html"><![CDATA[<p>Dependency injection containers look more mysterious than they are.</p>
<p>That is partly because mature frameworks have a lot of surface area: scopes, decorators, open generics, factories, validation, source generators, keyed services, child containers, disposal rules, Unity scene integration, and enough extension methods to make autocomplete tired. Useful tools, yes. Magic, no.</p>
<p>At the core, a DI container is an object-graph builder. It stores instructions that say “when someone asks for this type, build that type.” Then, when asked for an object, it walks the constructor parameters, builds the dependencies first, and finally calls the constructor.</p>
<p>That is the part worth understanding.</p>
<p>This article builds a small DI framework from scratch in C#. Not a production framework. Not a replacement for Microsoft.Extensions.DependencyInjection, Autofac, VContainer, Reflex, or whatever your project already uses. A teaching container. Small enough to read in one sitting, functional enough to explain what actually happens inside the box.</p>
<p><img src="/images/posts/build-a-simple-di-container-in-csharp/diagram.svg" alt="DI container construction flow"></p>
<h2 id="the-ordinary-problem">The ordinary problem</h2>
<p>A lot of C# code starts like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Player</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> InventoryService inventory = <span style="color:#66d9ef">new</span> InventoryService();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Pickup(Item item)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        inventory.Add(item);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That looks harmless because it is harmless when the project is tiny. The player needs inventory behavior, so it creates an inventory service.</p>
<p>Then the inventory service grows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">InventoryService</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> SaveService save = <span style="color:#66d9ef">new</span> SaveService();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> AudioService audio = <span style="color:#66d9ef">new</span> AudioService();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Add(Item item)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Add the item.</span>
</span></span><span style="display:flex;"><span>        save.MarkDirty();
</span></span><span style="display:flex;"><span>        audio.Play(<span style="color:#e6db74">&#34;pickup&#34;</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now <code>Player</code> is not only coupled to <code>InventoryService</code>. It is indirectly coupled to <code>SaveService</code> and <code>AudioService</code>, because constructing inventory constructs those too. If <code>AudioService</code> later needs configuration, the dependency chain keeps spreading.</p>
<p>Testing also gets annoying. A simple player test now creates real save and audio services unless the code is rewritten. That is how a five-line unit test accidentally starts touching disk, playing sounds, loading assets, or depending on Unity scene state. Congratulations, the test now has side quests.</p>
<p>The first fix is constructor injection:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Player</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IInventoryService inventory;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Player(IInventoryService inventory)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.inventory = inventory;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Pickup(Item item)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        inventory.Add(item);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now <code>Player</code> says what it needs. It does not know how inventory is built. It does not know whether the inventory implementation saves to disk, memory, cloud storage, or a fake test double.</p>
<p>That is dependency injection. A class receives its dependencies from the outside instead of constructing or locating them itself.</p>
<p>But this creates the next question:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> save = <span style="color:#66d9ef">new</span> SaveService();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> audio = <span style="color:#66d9ef">new</span> AudioService();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> inventory = <span style="color:#66d9ef">new</span> InventoryService(save, audio);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> player = <span style="color:#66d9ef">new</span> Player(inventory);
</span></span></code></pre></div><p>Something still needs to create <code>SaveService</code>, create <code>AudioService</code>, create <code>InventoryService</code>, create <code>Player</code>, and connect the graph in the right order.</p>
<p>For a small program, writing those <code>new</code> calls manually is fine. Do not over-engineer breakfast. But when the graph grows, teams often want one place to register construction rules and one resolver that can build the graph on demand.</p>
<p>That is the problem our tiny DI container will solve.</p>
<h2 id="stage-1-service-registration">Stage 1: service registration</h2>
<p>Start with the smallest useful registration API:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>container.Register&lt;IInventoryService, InventoryService&gt;();
</span></span></code></pre></div><p>Conceptually, the container stores this mapping:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>IInventoryService -&gt; InventoryService
</span></span></code></pre></div><p>When someone asks for <code>IInventoryService</code>, the container should construct <code>InventoryService</code>.</p>
<p>Interfaces are common because they give the caller a narrow contract. <code>Player</code> should not care whether inventory is stored in memory, a save file, or a server-backed profile. It only needs <code>IInventoryService</code>.</p>
<p>But interfaces should not be required for everything. Sometimes a concrete class is already the right contract:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>container.Register&lt;GameManager, GameManager&gt;();
</span></span></code></pre></div><p>That looks silly enough that we can support a shorter version later:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>container.Register&lt;GameManager&gt;();
</span></span></code></pre></div><p>Internally, we need a dictionary keyed by the requested type.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Container</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> Dictionary&lt;Type, ServiceDescriptor&gt; services = <span style="color:#66d9ef">new</span>();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ServiceDescriptor</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> ServiceDescriptor(Type serviceType, Type implementationType)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        ServiceType = serviceType;
</span></span><span style="display:flex;"><span>        ImplementationType = implementationType;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Type ServiceType { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Type ImplementationType { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A <code>ServiceDescriptor</code> is just the container&rsquo;s note to itself. It says which type callers ask for and which concrete type should be built.</p>
<p>Now add registration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Container</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> Dictionary&lt;Type, ServiceDescriptor&gt; services = <span style="color:#66d9ef">new</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Register&lt;TService, TImplementation&gt;()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">where</span> TImplementation : TService
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        services[<span style="color:#66d9ef">typeof</span>(TService)] = <span style="color:#66d9ef">new</span> ServiceDescriptor(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(TImplementation));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Register&lt;TService&gt;()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        services[<span style="color:#66d9ef">typeof</span>(TService)] = <span style="color:#66d9ef">new</span> ServiceDescriptor(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(TService));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is the first piece of a DI framework. Very glamorous. A dictionary.</p>
<p>The <code>where TImplementation : TService</code> constraint catches obvious mistakes at compile time. This should compile:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>container.Register&lt;IInventoryService, InventoryService&gt;();
</span></span></code></pre></div><p>This should not:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>container.Register&lt;IInventoryService, AudioService&gt;();
</span></span></code></pre></div><p>The generic constraint does not solve every runtime problem, but it prevents one silly category of registration bug.</p>
<h2 id="stage-2-service-resolution">Stage 2: service resolution</h2>
<p>Registration is only half the job. The container also needs to resolve a service:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> inventory = container.Resolve&lt;IInventoryService&gt;();
</span></span></code></pre></div><p>The first version can be naive. Look up the requested type, get the implementation type, and call its constructor.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> TService Resolve&lt;TService&gt;()
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> (TService)Resolve(<span style="color:#66d9ef">typeof</span>(TService));
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">object</span> Resolve(Type serviceType)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (!services.TryGetValue(serviceType, <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> descriptor))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(<span style="color:#e6db74">$&#34;No registration for {serviceType.Name}&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> Activator.CreateInstance(descriptor.ImplementationType)!;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>Activator.CreateInstance</code> is part of the standard library. It creates an instance of a type at runtime. For now, this only works for classes with a public parameterless constructor.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IInventoryService</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Add(Item item);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">InventoryService</span> : IInventoryService
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Add(Item item)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Store item.</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This works:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> container = <span style="color:#66d9ef">new</span> Container();
</span></span><span style="display:flex;"><span>container.Register&lt;IInventoryService, InventoryService&gt;();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> inventory = container.Resolve&lt;IInventoryService&gt;();
</span></span></code></pre></div><p>The container looked up <code>IInventoryService</code>, found <code>InventoryService</code>, and created one.</p>
<p>Tiny, but real.</p>
<h2 id="stage-3-constructors-with-dependencies">Stage 3: constructors with dependencies</h2>
<p>The naive resolver breaks as soon as <code>InventoryService</code> needs something else:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">InventoryService</span> : IInventoryService
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ISaveService save;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> InventoryService(ISaveService save)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.save = save;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>Activator.CreateInstance(type)</code> only calls a parameterless constructor. There is no parameterless constructor now. The container needs to inspect the constructor, inspect its parameters, resolve those parameters, then call the constructor with the resolved objects.</p>
<p>This is the heart of automatic constructor injection.</p>
<p>C# reflection gives us metadata about types at runtime. A <code>Type</code> can tell us which constructors it has. A <code>ConstructorInfo</code> can tell us which parameters it expects. A <code>ParameterInfo</code> can tell us the type of each parameter.</p>
<p>Here is the next resolver:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">object</span> Resolve(Type serviceType)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (!services.TryGetValue(serviceType, <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> descriptor))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(<span style="color:#e6db74">$&#34;No registration for {serviceType.Name}&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> implementationType = descriptor.ImplementationType;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> constructor = SelectConstructor(implementationType);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> parameters = constructor.GetParameters();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> arguments = parameters
</span></span><span style="display:flex;"><span>        .Select(parameter =&gt; Resolve(parameter.ParameterType))
</span></span><span style="display:flex;"><span>        .ToArray();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> constructor.Invoke(arguments);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> ConstructorInfo SelectConstructor(Type implementationType)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> constructors = implementationType.GetConstructors();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (constructors.Length != <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">$&#34;Type {implementationType.Name} must have exactly one public constructor.&#34;</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> constructors[<span style="color:#ae81ff">0</span>];
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now this can work:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerService</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> PlayerService(
</span></span><span style="display:flex;"><span>        IInventoryService inventory,
</span></span><span style="display:flex;"><span>        IAudioService audio)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>When resolving <code>PlayerService</code>, the container walks this tree:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>PlayerService
</span></span><span style="display:flex;"><span>├── IInventoryService
</span></span><span style="display:flex;"><span>│   └── InventoryService
</span></span><span style="display:flex;"><span>└── IAudioService
</span></span><span style="display:flex;"><span>    └── AudioService
</span></span></code></pre></div><p>That tree is the object graph. Each node is an object the container needs to build. Constructor parameters are edges to other nodes.</p>
<p>The resolver is recursive because dependencies can have dependencies. If <code>InventoryService</code> needs <code>ISaveService</code>, resolving <code>PlayerService</code> becomes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>PlayerService
</span></span><span style="display:flex;"><span>├── IInventoryService
</span></span><span style="display:flex;"><span>│   └── InventoryService
</span></span><span style="display:flex;"><span>│       └── ISaveService
</span></span><span style="display:flex;"><span>│           └── SaveService
</span></span><span style="display:flex;"><span>└── IAudioService
</span></span><span style="display:flex;"><span>    └── AudioService
</span></span></code></pre></div><p>The code is not doing anything mystical. It is walking constructor metadata and recursively calling <code>Resolve</code> for each parameter.</p>
<p>This is also where error handling matters. If <code>InventoryService</code> asks for <code>ISaveService</code> and <code>ISaveService</code> has not been registered, the container should say that clearly. A vague reflection exception is not enough.</p>
<h2 id="stage-4-resolving-concrete-types">Stage 4: resolving concrete types</h2>
<p>The current container requires every type to be registered. That is simple, but it makes this annoying:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>container.Register&lt;GameManager&gt;();
</span></span><span style="display:flex;"><span>container.Register&lt;PlayerService&gt;();
</span></span><span style="display:flex;"><span>container.Register&lt;IInventoryService, InventoryService&gt;();
</span></span><span style="display:flex;"><span>container.Register&lt;IAudioService, AudioService&gt;();
</span></span><span style="display:flex;"><span>container.Register&lt;ISaveService, SaveService&gt;();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> game = container.Resolve&lt;GameManager&gt;();
</span></span></code></pre></div><p>Some containers allow unregistered concrete types to be constructed automatically. If you ask for a concrete class and there is no registration, the container treats the class as its own implementation.</p>
<p>That means <code>GameManager</code> and <code>PlayerService</code> may not need explicit registrations, while interfaces still do.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> ServiceDescriptor GetDescriptor(Type serviceType)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (services.TryGetValue(serviceType, <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> descriptor))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> descriptor;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (!serviceType.IsAbstract &amp;&amp; !serviceType.IsInterface)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> ServiceDescriptor(serviceType, serviceType);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(<span style="color:#e6db74">$&#34;No registration for {serviceType.Name}&#34;</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then <code>Resolve</code> calls <code>GetDescriptor</code> instead of directly reading the dictionary.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">object</span> Resolve(Type serviceType)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> descriptor = GetDescriptor(serviceType);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> implementationType = descriptor.ImplementationType;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> constructor = SelectConstructor(implementationType);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> parameters = constructor.GetParameters();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> arguments = parameters
</span></span><span style="display:flex;"><span>        .Select(parameter =&gt; Resolve(parameter.ParameterType))
</span></span><span style="display:flex;"><span>        .ToArray();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> constructor.Invoke(arguments);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is a design choice. Auto-constructing concretes is convenient, especially for pure application services. Requiring explicit registration is stricter and can make the graph easier to audit. Production containers vary here.</p>
<p>For a learning container, allowing concrete self-binding keeps examples smaller while still forcing interfaces to be registered.</p>
<h2 id="stage-5-lifetimes">Stage 5: lifetimes</h2>
<p>So far, every resolve creates a new instance. That is called a transient lifetime.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Resolve&lt;IInventoryService&gt;() -&gt; new InventoryService
</span></span><span style="display:flex;"><span>Resolve&lt;IInventoryService&gt;() -&gt; another new InventoryService
</span></span><span style="display:flex;"><span>Resolve&lt;IInventoryService&gt;() -&gt; another new InventoryService
</span></span></code></pre></div><p>Sometimes that is exactly right. A command handler, small calculation service, temporary presenter, or short-lived object can be transient.</p>
<p>Other services should be shared. A save service, audio mixer, configuration object, telemetry client, or random source may need one instance reused across the graph. That is a singleton lifetime.</p>
<p>Add a lifetime enum:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">enum</span> ServiceLifetime
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    Transient,
</span></span><span style="display:flex;"><span>    Singleton
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Extend the descriptor:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ServiceDescriptor</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> ServiceDescriptor(
</span></span><span style="display:flex;"><span>        Type serviceType,
</span></span><span style="display:flex;"><span>        Type implementationType,
</span></span><span style="display:flex;"><span>        ServiceLifetime lifetime)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        ServiceType = serviceType;
</span></span><span style="display:flex;"><span>        ImplementationType = implementationType;
</span></span><span style="display:flex;"><span>        Lifetime = lifetime;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Type ServiceType { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Type ImplementationType { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> ServiceLifetime Lifetime { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">object?</span> SingletonInstance { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Add registration helpers:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> RegisterTransient&lt;TService, TImplementation&gt;()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">where</span> TImplementation : TService
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    services[<span style="color:#66d9ef">typeof</span>(TService)] = <span style="color:#66d9ef">new</span> ServiceDescriptor(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">typeof</span>(TImplementation),
</span></span><span style="display:flex;"><span>        ServiceLifetime.Transient);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> RegisterSingleton&lt;TService, TImplementation&gt;()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">where</span> TImplementation : TService
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    services[<span style="color:#66d9ef">typeof</span>(TService)] = <span style="color:#66d9ef">new</span> ServiceDescriptor(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">typeof</span>(TImplementation),
</span></span><span style="display:flex;"><span>        ServiceLifetime.Singleton);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> RegisterTransient&lt;TService&gt;()
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    services[<span style="color:#66d9ef">typeof</span>(TService)] = <span style="color:#66d9ef">new</span> ServiceDescriptor(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>        ServiceLifetime.Transient);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> RegisterSingleton&lt;TService&gt;()
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    services[<span style="color:#66d9ef">typeof</span>(TService)] = <span style="color:#66d9ef">new</span> ServiceDescriptor(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>        ServiceLifetime.Singleton);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now update resolution:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">object</span> Resolve(Type serviceType)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> descriptor = GetDescriptor(serviceType);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (descriptor.Lifetime == ServiceLifetime.Singleton &amp;&amp;
</span></span><span style="display:flex;"><span>        descriptor.SingletonInstance != <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> descriptor.SingletonInstance;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> instance = CreateInstance(descriptor.ImplementationType);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (descriptor.Lifetime == ServiceLifetime.Singleton)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        descriptor.SingletonInstance = instance;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> instance;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Move constructor work into <code>CreateInstance</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">object</span> CreateInstance(Type implementationType)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> constructor = SelectConstructor(implementationType);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> parameters = constructor.GetParameters();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> arguments = parameters
</span></span><span style="display:flex;"><span>        .Select(parameter =&gt; Resolve(parameter.ParameterType))
</span></span><span style="display:flex;"><span>        .ToArray();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> constructor.Invoke(arguments);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The singleton cache lives on the descriptor. First resolve creates the instance. Later resolves return the cached object.</p>
<p>Scoped lifetimes are the next common feature in mature containers. A scoped service is reused inside one scope, such as one web request, one Unity scene, or one game match, but not globally forever. This article will not implement scopes because the simple container is already doing its teaching job. Just remember that scoped lifetime sits between transient and singleton.</p>
<h2 id="stage-6-registering-existing-instances">Stage 6: registering existing instances</h2>
<p>Sometimes the container should not construct the object. The object already exists.</p>
<p>In Unity, this matters because scene objects and <code>MonoBehaviour</code> components are usually created by Unity. Outside Unity, this is useful for configuration values, logging sinks, or objects created by platform code.</p>
<p>Add instance registration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> RegisterInstance&lt;TService&gt;(TService instance)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">where</span> TService : notnull
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    services[<span style="color:#66d9ef">typeof</span>(TService)] = <span style="color:#66d9ef">new</span> ServiceDescriptor(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>        instance.GetType(),
</span></span><span style="display:flex;"><span>        ServiceLifetime.Singleton)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        SingletonInstance = instance
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now a prebuilt object can become part of the graph:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> settings = <span style="color:#66d9ef">new</span> GameSettings
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    SaveSlot = <span style="color:#e6db74">&#34;profile-1&#34;</span>,
</span></span><span style="display:flex;"><span>    MasterVolume = <span style="color:#ae81ff">0.8f</span>
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>container.RegisterInstance(settings);
</span></span></code></pre></div><p>The container resolves <code>GameSettings</code> like any other singleton, but it does not own construction.</p>
<p>This is a tiny feature, but it connects the clean C# world to the messy real world where objects sometimes come from files, engines, scenes, command-line arguments, or platform callbacks.</p>
<h2 id="stage-7-recursive-dependency-graphs">Stage 7: recursive dependency graphs</h2>
<p>With constructor injection, singleton caching, and concrete self-binding, the container can build a real graph.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>GameManager
</span></span><span style="display:flex;"><span>└── PlayerService
</span></span><span style="display:flex;"><span>    ├── InventoryService
</span></span><span style="display:flex;"><span>    │   └── SaveService
</span></span><span style="display:flex;"><span>    └── AudioService
</span></span></code></pre></div><p>The code might look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">GameManager</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> PlayerService player;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> GameManager(PlayerService player)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.player = player;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerService</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> PlayerService(
</span></span><span style="display:flex;"><span>        IInventoryService inventory,
</span></span><span style="display:flex;"><span>        IAudioService audio)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">InventoryService</span> : IInventoryService
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> InventoryService(ISaveService save)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The composition root registers the interfaces:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> container = <span style="color:#66d9ef">new</span> Container();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>container.RegisterSingleton&lt;ISaveService, SaveService&gt;();
</span></span><span style="display:flex;"><span>container.RegisterSingleton&lt;IAudioService, AudioService&gt;();
</span></span><span style="display:flex;"><span>container.RegisterTransient&lt;IInventoryService, InventoryService&gt;();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> game = container.Resolve&lt;GameManager&gt;();
</span></span></code></pre></div><p><code>GameManager</code> and <code>PlayerService</code> are concrete, so this teaching container can construct them without explicit registration. The interfaces need mappings.</p>
<p>The resolver effectively does this:</p>
<ol>
<li>Resolve <code>GameManager</code>.</li>
<li>Its constructor needs <code>PlayerService</code>.</li>
<li>Resolve <code>PlayerService</code>.</li>
<li>Its constructor needs <code>IInventoryService</code> and <code>IAudioService</code>.</li>
<li>Resolve <code>IInventoryService</code> using the <code>InventoryService</code> registration.</li>
<li><code>InventoryService</code> needs <code>ISaveService</code>.</li>
<li>Resolve <code>ISaveService</code> using the <code>SaveService</code> registration.</li>
<li>Resolve <code>IAudioService</code> using the <code>AudioService</code> registration.</li>
<li>Call constructors on the way back up.</li>
</ol>
<p>The last part is important. The container walks down the dependency graph first, then constructs objects once their constructor arguments exist.</p>
<p>That is why DI containers are often described as object graph builders.</p>
<h2 id="stage-8-circular-dependencies">Stage 8: circular dependencies</h2>
<p>A naive recursive resolver has one obvious failure mode: circular dependencies.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ServiceA</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> ServiceA(ServiceB b) { }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ServiceB</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> ServiceB(ServiceA a) { }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The graph is impossible:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>ServiceA
</span></span><span style="display:flex;"><span>└── ServiceB
</span></span><span style="display:flex;"><span>    └── ServiceA
</span></span><span style="display:flex;"><span>        └── ServiceB
</span></span><span style="display:flex;"><span>            └── ...
</span></span></code></pre></div><p>A naive resolver keeps calling itself until the stack blows up. Helpful error message? None. Just a crash and a developer muttering at the monitor.</p>
<p>Add a resolution stack.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">object</span> Resolve(Type serviceType, Stack&lt;Type&gt; stack)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (stack.Contains(serviceType))
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> chain = <span style="color:#66d9ef">string</span>.Join(<span style="color:#e6db74">&#34; -&gt; &#34;</span>, stack.Reverse().Select(type =&gt; type.Name));
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">$&#34;Circular dependency detected: {chain} -&gt; {serviceType.Name}&#34;</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    stack.Push(serviceType);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> descriptor = GetDescriptor(serviceType);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (descriptor.Lifetime == ServiceLifetime.Singleton &amp;&amp;
</span></span><span style="display:flex;"><span>            descriptor.SingletonInstance != <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> descriptor.SingletonInstance;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> instance = CreateInstance(descriptor.ImplementationType, stack);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (descriptor.Lifetime == ServiceLifetime.Singleton)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            descriptor.SingletonInstance = instance;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> instance;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">finally</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        stack.Pop();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Update the public entry point:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> TService Resolve&lt;TService&gt;()
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> (TService)Resolve(<span style="color:#66d9ef">typeof</span>(TService), <span style="color:#66d9ef">new</span> Stack&lt;Type&gt;());
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>And pass the stack through constructor resolution:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">object</span> CreateInstance(Type implementationType, Stack&lt;Type&gt; stack)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> constructor = SelectConstructor(implementationType);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> parameters = constructor.GetParameters();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> arguments = parameters
</span></span><span style="display:flex;"><span>        .Select(parameter =&gt; Resolve(parameter.ParameterType, stack))
</span></span><span style="display:flex;"><span>        .ToArray();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> constructor.Invoke(arguments);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now the impossible graph produces a useful message:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Circular dependency detected: ServiceA -&gt; ServiceB -&gt; ServiceA
</span></span></code></pre></div><p>That is the difference between “reflection exploded” and “your graph has a cycle here.” Production containers spend a lot of effort on this kind of diagnosis because the container sits at a confusing boundary. Good errors save real time.</p>
<h2 id="stage-9-multiple-constructors">Stage 9: multiple constructors</h2>
<p>Our <code>SelectConstructor</code> method currently requires exactly one public constructor. That is deliberately strict.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> ConstructorInfo SelectConstructor(Type implementationType)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> constructors = implementationType.GetConstructors();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (constructors.Length != <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">$&#34;Type {implementationType.Name} must have exactly one public constructor.&#34;</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> constructors[<span style="color:#ae81ff">0</span>];
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Many production containers choose the “greediest” constructor they can satisfy. Some use attributes. Some prefer the constructor with the most parameters. Some integrate source generators.</p>
<p>For a small container, strict is better. If a type has two public constructors, the container refuses to guess.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerService</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> PlayerService(IInventoryService inventory) { }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> PlayerService(IInventoryService inventory, IAudioService audio) { }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Which one should be used? A human can answer that. The tiny container should not pretend it knows.</p>
<p>If you wanted to extend this, a simple next step would be an attribute:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[AttributeUsage(AttributeTargets.Constructor)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">InjectAttribute</span> : Attribute
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then <code>SelectConstructor</code> could prefer the one marked <code>[Inject]</code>. That is how many frameworks handle ambiguity. But it is extra code, so it only belongs in the teaching container if ambiguity becomes a real teaching point.</p>
<p>Lazy code stops before it builds features it does not need. Shocking discipline, apparently.</p>
<h2 id="stage-10-the-finished-small-container">Stage 10: the finished small container</h2>
<p>Here is the container after the pieces above are assembled:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Collections.Generic;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Linq;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Reflection;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">enum</span> ServiceLifetime
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    Transient,
</span></span><span style="display:flex;"><span>    Singleton
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ServiceDescriptor</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> ServiceDescriptor(
</span></span><span style="display:flex;"><span>        Type serviceType,
</span></span><span style="display:flex;"><span>        Type implementationType,
</span></span><span style="display:flex;"><span>        ServiceLifetime lifetime)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        ServiceType = serviceType;
</span></span><span style="display:flex;"><span>        ImplementationType = implementationType;
</span></span><span style="display:flex;"><span>        Lifetime = lifetime;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Type ServiceType { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Type ImplementationType { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> ServiceLifetime Lifetime { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">object?</span> SingletonInstance { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Container</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> Dictionary&lt;Type, ServiceDescriptor&gt; services = <span style="color:#66d9ef">new</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> RegisterTransient&lt;TService, TImplementation&gt;()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">where</span> TImplementation : TService
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        services[<span style="color:#66d9ef">typeof</span>(TService)] = <span style="color:#66d9ef">new</span> ServiceDescriptor(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(TImplementation),
</span></span><span style="display:flex;"><span>            ServiceLifetime.Transient);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> RegisterSingleton&lt;TService, TImplementation&gt;()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">where</span> TImplementation : TService
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        services[<span style="color:#66d9ef">typeof</span>(TService)] = <span style="color:#66d9ef">new</span> ServiceDescriptor(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(TImplementation),
</span></span><span style="display:flex;"><span>            ServiceLifetime.Singleton);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> RegisterTransient&lt;TService&gt;()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        services[<span style="color:#66d9ef">typeof</span>(TService)] = <span style="color:#66d9ef">new</span> ServiceDescriptor(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>            ServiceLifetime.Transient);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> RegisterSingleton&lt;TService&gt;()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        services[<span style="color:#66d9ef">typeof</span>(TService)] = <span style="color:#66d9ef">new</span> ServiceDescriptor(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>            ServiceLifetime.Singleton);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> RegisterInstance&lt;TService&gt;(TService instance)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">where</span> TService : notnull
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        services[<span style="color:#66d9ef">typeof</span>(TService)] = <span style="color:#66d9ef">new</span> ServiceDescriptor(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(TService),
</span></span><span style="display:flex;"><span>            instance.GetType(),
</span></span><span style="display:flex;"><span>            ServiceLifetime.Singleton)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            SingletonInstance = instance
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> TService Resolve&lt;TService&gt;()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> (TService)Resolve(<span style="color:#66d9ef">typeof</span>(TService), <span style="color:#66d9ef">new</span> Stack&lt;Type&gt;());
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">object</span> Resolve(Type serviceType, Stack&lt;Type&gt; stack)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (stack.Contains(serviceType))
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> chain = <span style="color:#66d9ef">string</span>.Join(<span style="color:#e6db74">&#34; -&gt; &#34;</span>, stack.Reverse().Select(type =&gt; type.Name));
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">$&#34;Circular dependency detected: {chain} -&gt; {serviceType.Name}&#34;</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        stack.Push(serviceType);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> descriptor = GetDescriptor(serviceType);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> (descriptor.Lifetime == ServiceLifetime.Singleton &amp;&amp;
</span></span><span style="display:flex;"><span>                descriptor.SingletonInstance != <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span> descriptor.SingletonInstance;
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> instance = CreateInstance(descriptor.ImplementationType, stack);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> (descriptor.Lifetime == ServiceLifetime.Singleton)
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                descriptor.SingletonInstance = instance;
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> instance;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">finally</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            stack.Pop();
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> ServiceDescriptor GetDescriptor(Type serviceType)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (services.TryGetValue(serviceType, <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> descriptor))
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> descriptor;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (!serviceType.IsAbstract &amp;&amp; !serviceType.IsInterface)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> ServiceDescriptor(
</span></span><span style="display:flex;"><span>                serviceType,
</span></span><span style="display:flex;"><span>                serviceType,
</span></span><span style="display:flex;"><span>                ServiceLifetime.Transient);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(<span style="color:#e6db74">$&#34;No registration for {serviceType.Name}&#34;</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">object</span> CreateInstance(Type implementationType, Stack&lt;Type&gt; stack)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> constructor = SelectConstructor(implementationType);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> parameters = constructor.GetParameters();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> arguments = parameters
</span></span><span style="display:flex;"><span>            .Select(parameter =&gt; Resolve(parameter.ParameterType, stack))
</span></span><span style="display:flex;"><span>            .ToArray();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> constructor.Invoke(arguments);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> ConstructorInfo SelectConstructor(Type implementationType)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> constructors = implementationType.GetConstructors();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (constructors.Length != <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">$&#34;Type {implementationType.Name} must have exactly one public constructor.&#34;</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> constructors[<span style="color:#ae81ff">0</span>];
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is not production-ready, but it is a real container. It supports:</p>
<ul>
<li>type registration</li>
<li>interface-to-implementation mappings</li>
<li>concrete type construction</li>
<li>automatic constructor injection</li>
<li>recursive dependency resolution</li>
<li>transient lifetime</li>
<li>singleton lifetime</li>
<li>instance registration</li>
<li>circular dependency detection</li>
<li>basic missing-registration errors</li>
<li>strict multiple-constructor handling</li>
</ul>
<p>That is enough to answer the original question: what actually happens inside a dependency injection framework?</p>
<p>A registration table plus recursive constructor calls. Less wizardry than advertised.</p>
<h2 id="composition-root-where-new-still-belongs">Composition root: where <code>new</code> still belongs</h2>
<p>DI does not mean “never use <code>new</code>.” That is a beginner misunderstanding, and it causes awful code.</p>
<p>There should still be a place where the application configures its dependency graph. That place is the composition root.</p>
<p>Plain English: the composition root is the startup area where the app decides which concrete classes should be used for the contracts the rest of the app depends on.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> container = <span style="color:#66d9ef">new</span> Container();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>container.RegisterSingleton&lt;ISaveService, SaveService&gt;();
</span></span><span style="display:flex;"><span>container.RegisterSingleton&lt;IAudioService, AudioService&gt;();
</span></span><span style="display:flex;"><span>container.RegisterTransient&lt;IInventoryService, InventoryService&gt;();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> game = container.Resolve&lt;GameManager&gt;();
</span></span><span style="display:flex;"><span>game.Start();
</span></span></code></pre></div><p>That code is allowed to know concrete types. That is its job. It is the wiring area.</p>
<p>The problem is not <code>new SaveService()</code> existing somewhere. The problem is <code>new SaveService()</code> appearing inside every gameplay class that happens to need saving. Put wiring in one place. Keep behavior classes focused on behavior.</p>
<p>A DI container helps when the graph is large enough that manually ordering all constructors becomes annoying. If the graph is small, manual composition is often clearer:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> save = <span style="color:#66d9ef">new</span> SaveService();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> audio = <span style="color:#66d9ef">new</span> AudioService();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> inventory = <span style="color:#66d9ef">new</span> InventoryService(save);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> player = <span style="color:#66d9ef">new</span> PlayerService(inventory, audio);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> game = <span style="color:#66d9ef">new</span> GameManager(player);
</span></span></code></pre></div><p>That is still dependency injection. No container required. The container is a convenience tool, not the definition of DI.</p>
<h2 id="constructor-injection-versus-field-and-property-injection">Constructor injection versus field and property injection</h2>
<p>This teaching container only supports constructor injection. That is intentional.</p>
<p>Constructor injection has a useful property: it makes required dependencies impossible to ignore. If a class cannot work without inventory and audio, its constructor says so.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> PlayerService(IInventoryService inventory, IAudioService audio)
</span></span></code></pre></div><p>Field injection and property injection are different:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerHud</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [Inject]</span> <span style="color:#66d9ef">private</span> IAudioService audio = <span style="color:#66d9ef">null</span>!;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That can be useful in Unity because Unity creates <code>MonoBehaviour</code> components. You often cannot call a custom constructor on them. A framework may create the component through Unity, then inject fields or methods afterward.</p>
<p>The cost is that required dependencies are less obvious. The constructor does not list them. The object can briefly exist in an uninjected state. Tests may need special setup. Nullability gets messier.</p>
<p>Use constructor injection for ordinary C# services. Use field, property, or method injection at engine boundaries only when the platform forces your hand. Unity is exactly such a platform. Still, do not spread field injection everywhere just because an attribute looks tidy.</p>
<h2 id="service-locator-is-not-the-same-thing">Service locator is not the same thing</h2>
<p>A global <code>Resolve&lt;T&gt;()</code> call is where DI containers go to become service locators.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerService</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Pickup(Item item)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> inventory = GlobalContainer.Resolve&lt;IInventoryService&gt;();
</span></span><span style="display:flex;"><span>        inventory.Add(item);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is not dependency injection. The class is still hunting for its dependency. It just hunts through a container instead of a singleton.</p>
<p>The dependency is hidden. The constructor does not reveal it. The test has to configure global state. The call can happen anywhere at any time. The code is easy to write and annoying to reason about.</p>
<p>Prefer this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerService</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IInventoryService inventory;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> PlayerService(IInventoryService inventory)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.inventory = inventory;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The container should be used at the composition root and framework boundaries. It should not become a vending machine every class carries around.</p>
<h2 id="reflection-overhead-and-constructor-caching">Reflection overhead and constructor caching</h2>
<p>Reflection has overhead. <code>GetConstructors</code>, <code>GetParameters</code>, and <code>ConstructorInfo.Invoke</code> are slower than direct <code>new</code> calls.</p>
<p>Do not panic. Object graph construction usually happens at startup, scene load, request start, or feature initialization. It is rarely in the inner gameplay loop. If you are resolving services every frame, the reflection overhead is not the first problem. The first problem is resolving services every frame.</p>
<p>Still, mature containers optimize this. Common approaches include:</p>
<ul>
<li>caching selected constructors</li>
<li>caching parameter metadata</li>
<li>compiling expression trees into factory delegates</li>
<li>generating factories at build time</li>
<li>using source generators</li>
</ul>
<p>Our container could add a tiny constructor cache:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> Dictionary&lt;Type, ConstructorInfo&gt; constructorCache = <span style="color:#66d9ef">new</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">private</span> ConstructorInfo GetConstructor(Type implementationType)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (constructorCache.TryGetValue(implementationType, <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> cached))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> cached;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> selected = SelectConstructor(implementationType);
</span></span><span style="display:flex;"><span>    constructorCache[implementationType] = selected;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> selected;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then <code>CreateInstance</code> calls <code>GetConstructor</code> instead of <code>SelectConstructor</code> directly.</p>
<p>That does not remove <code>ConstructorInfo.Invoke</code>, but it avoids rediscovering metadata every time. Good enough for a teaching container. Production frameworks go further because they must handle larger graphs and stricter performance budgets.</p>
<h2 id="unity-changes-construction-rules">Unity changes construction rules</h2>
<p>The container above is ordinary C#. That is good. Understanding DI before Unity enters the room keeps the topic cleaner.</p>
<p>Unity complicates DI because Unity controls many object lifetimes:</p>
<ul>
<li><code>MonoBehaviour</code> components are created by Unity.</li>
<li>Prefabs are instantiated through Unity.</li>
<li>Scene loading creates objects before your plain C# composition root may see them.</li>
<li>Serialized fields are assigned by the editor.</li>
<li><code>GameObject.AddComponent&lt;T&gt;()</code> does not call your custom constructor.</li>
</ul>
<p>So this is usually wrong:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerController</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> PlayerController(IInventoryService inventory)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The constructor is not the right Unity boundary.</p>
<p>Common Unity-friendly approaches include:</p>
<ol>
<li><strong>Keep ordinary services as plain C# classes.</strong> Use constructor injection there.</li>
<li><strong>Inject MonoBehaviours after Unity creates them.</strong> Field or method injection can work at the boundary.</li>
<li><strong>Use scene-level containers.</strong> A dungeon scene may have services and factories that live only for that scene.</li>
<li><strong>Use project-level containers.</strong> Audio, telemetry, platform services, and global settings may live across scenes.</li>
<li><strong>Register existing Unity objects as instances.</strong> A serialized <code>DungeonHudView</code> or scene camera can be registered instead of constructed.</li>
<li><strong>Keep runtime data out of the container.</strong> A current enemy position or current damage amount is request data, not a service.</li>
</ol>
<p>A Unity composition root might register scene objects like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>container.RegisterInstance(hudView);
</span></span><span style="display:flex;"><span>container.RegisterSingleton&lt;ISaveService, SaveService&gt;();
</span></span><span style="display:flex;"><span>container.RegisterSingleton&lt;IAudioService, UnityAudioService&gt;();
</span></span><span style="display:flex;"><span>container.RegisterTransient&lt;IInventoryService, InventoryService&gt;();
</span></span></code></pre></div><p>Then a scene bootstrapper could create ordinary C# systems:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> player = container.Resolve&lt;PlayerService&gt;();
</span></span></code></pre></div><p>For <code>MonoBehaviour</code> instances, a real Unity DI framework may do post-construction injection:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerHud</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> IInventoryService inventory = <span style="color:#66d9ef">null</span>!;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Construct(IInventoryService inventory)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.inventory = inventory;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Our tiny container does not implement method injection. It could, by reflecting over methods named <code>Construct</code> or marked with an attribute. But that is a Unity-specific extension, not the core lesson.</p>
<p>Frameworks such as VContainer and Reflex exist partly because Unity needs these extra integration points. Reflex, for example, has concepts around containers, <code>ContainerBuilder</code>, lifetimes, scene scopes, installers, and injection into Unity-created objects. Those features sit on top of the same fundamentals: registration, mapping, resolution, construction, and lifetime management.</p>
<h2 id="what-mature-frameworks-add">What mature frameworks add</h2>
<p>The container in this article is intentionally small. Mature frameworks add features because real applications have annoying edges.</p>
<p>Microsoft&rsquo;s dependency injection system focuses on a straightforward service collection, constructor injection, and lifetimes. Autofac adds a richer container model with modules, scopes, factories, decorators, and more advanced registration patterns. VContainer focuses on fast Unity-friendly DI with source generation options and strong Unity integration. Reflex is a minimal Unity DI framework with container builders, scene scopes, installers, and Unity injection support.</p>
<p>Different style, same core idea.</p>
<p>Underneath, the flow still looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Registration
</span></span><span style="display:flex;"><span>      ↓
</span></span><span style="display:flex;"><span>Type / Factory Mapping
</span></span><span style="display:flex;"><span>      ↓
</span></span><span style="display:flex;"><span>Dependency Resolution
</span></span><span style="display:flex;"><span>      ↓
</span></span><span style="display:flex;"><span>Object Graph Construction
</span></span><span style="display:flex;"><span>      ↓
</span></span><span style="display:flex;"><span>Lifetime Management
</span></span></code></pre></div><p>The mature framework version has better diagnostics, faster construction, scopes, disposal, factory registrations, open generics, validation, and Unity/editor integration. But if you understand the small version, the large version becomes less spooky. It is no longer magic. It is a lot of careful engineering around an object graph builder.</p>
<h2 id="the-architecture-tradeoff">The architecture tradeoff</h2>
<p>DI is not automatically good architecture.</p>
<p>It helps when dependencies are real, construction is repeated, testing matters, and the graph has enough size that central wiring is useful. It hurts when it creates interfaces that do not mean anything, moves simple <code>new</code> calls into a container for no benefit, or encourages every class to resolve things globally.</p>
<p>A small value object does not need DI:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> damage = <span style="color:#66d9ef">new</span> DamageAmount(<span style="color:#ae81ff">12</span>);
</span></span></code></pre></div><p>A simple helper probably does not need DI either. If a class has no dependencies and no meaningful replacement, registering it in a container may only make the code harder to follow.</p>
<p>Use DI to manage stable collaborators:</p>
<ul>
<li>save services</li>
<li>audio services</li>
<li>repositories</li>
<li>gameplay rules</li>
<li>factories</li>
<li>platform adapters</li>
<li>telemetry sinks</li>
<li>configuration objects</li>
</ul>
<p>Do not use DI to hide ordinary local decisions. Local state can stay local. Temporary values can stay method parameters. Some objects should just be created directly.</p>
<h2 id="the-useful-mental-model">The useful mental model</h2>
<p>A DI container is an object-graph builder with memory.</p>
<p>Registration tells it what concrete types or instances correspond to service contracts. Resolution asks it to build a requested type. Constructor injection tells it which dependencies the type needs. Recursion lets it build dependencies before dependents. Lifetimes decide whether each result is new or cached. Circular dependency detection stops impossible graphs from becoming stack overflows.</p>
<p>That is it.</p>
<p>The tiny framework here is not production-ready, but it removes the magic trick. When a mature framework resolves <code>GameManager</code>, it is doing a more polished version of the same process:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Resolve GameManager
</span></span><span style="display:flex;"><span>  inspect constructor
</span></span><span style="display:flex;"><span>  resolve PlayerService
</span></span><span style="display:flex;"><span>    inspect constructor
</span></span><span style="display:flex;"><span>    resolve IInventoryService
</span></span><span style="display:flex;"><span>      construct InventoryService
</span></span><span style="display:flex;"><span>        resolve ISaveService
</span></span><span style="display:flex;"><span>    resolve IAudioService
</span></span><span style="display:flex;"><span>  construct PlayerService
</span></span><span style="display:flex;"><span>construct GameManager
</span></span></code></pre></div><p>Once that clicks, DI becomes less of a buzzword and more of a boring tool. Good. Boring tools are easier to use correctly.</p>
<p>The point is not to worship containers. The point is to keep construction policy out of the classes doing the work. Build the graph at the edge. Pass dependencies in. Let the container help when the graph is large enough to justify it. And when the graph is still small, write the <code>new</code> calls by hand and move on with your life.</p>
<p>Related reading: <a href="/posts/dependency-injection-for-game-code/">Dependency Injection for Game Code</a> covers the architecture rule from a game-code point of view. <a href="/posts/dependency-injection-in-unity/">Dependency Injection in Unity</a> focuses on Unity-specific dependency management. <a href="/posts/reflex-di-factories-abstract-factories/">Combining Reflex DI with Factories</a> shows how a real Unity DI container fits with runtime factories.</p>
]]></content>
        </item>
        
        <item>
            <title>Combining Reflex DI with Factories: Runtime Creation Without Container Soup</title>
            <link>https://blog.jonmikeys.com/posts/reflex-di-factories-abstract-factories/</link>
            <pubDate>Sat, 05 Sep 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/reflex-di-factories-abstract-factories/</guid>
            <description>A practical follow-up on combining dependency injection with factory and abstract factory patterns in Unity, using Reflex as the container and one dungeon-room spawning context from start to finish.</description>
            <content type="html"><![CDATA[<p>Factories and dependency injection solve different parts of the same mess.</p>
<p>Dependency injection answers: <strong>where do stable collaborators come from?</strong> A combat director needs a random source, a telemetry sink, a damage rules service, and maybe a catalog of room definitions. It should receive those things instead of finding them through globals.</p>
<p>Factories answer: <strong>how do runtime objects get created after the graph already exists?</strong> A dungeon room needs enemies, pickups, traps, doors, VFX, and UI prompts. Those objects are not all created at boot. They appear later, usually because the player entered a room, a wave advanced, or a designer-authored definition asked for them.</p>
<p>The mistake is treating one tool like it replaces the other. A DI container should not become the thing every gameplay class asks whenever it wants a goblin. A factory should not rebuild half the service graph by hand. Used together, they give a cleaner split: Reflex wires the long-lived dependencies at the scene boundary, and factories create short-lived runtime objects from explicit requests.</p>
<p>This article uses one context the whole way through: a small dungeon room system. A room definition says which encounter archetype to run. The room runtime creates a matching family of systems: enemy spawns, loot, traps, and room UI. Reflex provides the scene container. A factory keeps room creation policy out of the gameplay loop. An abstract factory handles the cases where a room mode needs a consistent family of related objects.</p>
<p><img src="/images/posts/reflex-di-factories-abstract-factories/diagram.svg" alt="Reflex DI factory flow"></p>
<h2 id="the-context-dungeon-rooms-not-loose-examples">The context: dungeon rooms, not loose examples</h2>
<p>Imagine a top-down action RPG with procedural dungeon rooms. Each room is authored as data:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">enum</span> RoomArchetype
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    Skirmish,
</span></span><span style="display:flex;"><span>    Treasure,
</span></span><span style="display:flex;"><span>    TrapGauntlet,
</span></span><span style="display:flex;"><span>    Boss
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">[CreateAssetMenu(menuName = &#34;Dungeon/Room Definition&#34;)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">RoomDefinition</span> : ScriptableObject
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> RoomArchetype Archetype;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> EnemyPackId EnemyPack;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> LootTableId LootTable;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> TrapLayoutId TrapLayout;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> ThreatLevel;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>When the player enters a room, the game needs to build a runtime object that can manage that room. That runtime object may spawn enemies, start music, create rewards, arm traps, update the HUD, and report analytics.</p>
<p>Bad code usually starts like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DungeonRoomController</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> RoomDefinition definition;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> Start()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> enemyCatalog = EnemyCatalog.Instance;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> lootCatalog = LootCatalog.Instance;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> audio = AudioManager.Instance;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> rng = GlobalRandom.Instance;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Then a pile of branching creation code begins.</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This looks fast to write. It is not fast to maintain. The room controller now knows where every service lives, how every runtime object is created, and which branches belong to each room archetype. It will keep growing until nobody can tell whether they are changing room rules, enemy construction, trap construction, or UI setup.</p>
<p>The better split is boring:</p>
<ul>
<li>Reflex wires stable services and factories into the scene container.</li>
<li><code>DungeonRoomController</code> receives one room factory.</li>
<li>The room factory creates a room runtime from a <code>RoomDefinition</code>.</li>
<li>The runtime owns room behavior after creation.</li>
</ul>
<p>The controller becomes a Unity boundary, not a service locator with a transform.</p>
<h2 id="reflex-is-the-wiring-edge">Reflex is the wiring edge</h2>
<p>Reflex is a minimal dependency injection framework for Unity. The important pieces for this discussion are enough to keep straight:</p>
<ul>
<li><code>ContainerBuilder</code> registers values, types, and factories.</li>
<li><code>Lifetime.Singleton</code>, <code>Lifetime.Scoped</code>, and <code>Lifetime.Transient</code> control reuse.</li>
<li><code>Resolution.Lazy</code> and <code>Resolution.Eager</code> control when non-transient bindings are created.</li>
<li><code>ContainerScope</code> can install scene bindings through <code>IInstaller</code> components.</li>
<li><code>[Inject]</code> can inject fields, properties, or methods on Unity-facing objects.</li>
<li><code>Container.Construct&lt;T&gt;()</code> can construct plain C# classes with constructor injection.</li>
</ul>
<p>The exact API may evolve, but the architectural rule should not: Reflex belongs at the composition boundary. It should build the object graph. It should not become a random object vending machine called from every enemy, pickup, and projectile.</p>
<p>A simple scene installer for the dungeon might look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> Reflex.Core;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> Reflex.Enums;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DungeonInstaller</span> : MonoBehaviour, IInstaller
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> EnemyCatalog enemyCatalog;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> LootCatalog lootCatalog;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> TrapCatalog trapCatalog;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> DungeonHudView hudView;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> InstallBindings(ContainerBuilder builder)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        builder.RegisterValue(enemyCatalog, <span style="color:#66d9ef">new</span>[] { <span style="color:#66d9ef">typeof</span>(EnemyCatalog) });
</span></span><span style="display:flex;"><span>        builder.RegisterValue(lootCatalog, <span style="color:#66d9ef">new</span>[] { <span style="color:#66d9ef">typeof</span>(LootCatalog) });
</span></span><span style="display:flex;"><span>        builder.RegisterValue(trapCatalog, <span style="color:#66d9ef">new</span>[] { <span style="color:#66d9ef">typeof</span>(TrapCatalog) });
</span></span><span style="display:flex;"><span>        builder.RegisterValue(hudView, <span style="color:#66d9ef">new</span>[] { <span style="color:#66d9ef">typeof</span>(DungeonHudView) });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        builder.RegisterType(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(UnityRandom),
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">new</span>[] { <span style="color:#66d9ef">typeof</span>(IRandom) },
</span></span><span style="display:flex;"><span>            Lifetime.Singleton,
</span></span><span style="display:flex;"><span>            Resolution.Lazy);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        builder.RegisterType(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(UnityDungeonAudio),
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">new</span>[] { <span style="color:#66d9ef">typeof</span>(IDungeonAudio) },
</span></span><span style="display:flex;"><span>            Lifetime.Singleton,
</span></span><span style="display:flex;"><span>            Resolution.Lazy);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        builder.RegisterType(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(PrefabEnemyFactory),
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">new</span>[] { <span style="color:#66d9ef">typeof</span>(IEnemyFactory) },
</span></span><span style="display:flex;"><span>            Lifetime.Scoped,
</span></span><span style="display:flex;"><span>            Resolution.Lazy);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        builder.RegisterType(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(PrefabLootFactory),
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">new</span>[] { <span style="color:#66d9ef">typeof</span>(ILootFactory) },
</span></span><span style="display:flex;"><span>            Lifetime.Scoped,
</span></span><span style="display:flex;"><span>            Resolution.Lazy);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        builder.RegisterType(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(PrefabTrapFactory),
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">new</span>[] { <span style="color:#66d9ef">typeof</span>(ITrapFactory) },
</span></span><span style="display:flex;"><span>            Lifetime.Scoped,
</span></span><span style="display:flex;"><span>            Resolution.Lazy);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        builder.RegisterType(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">typeof</span>(DungeonRoomFactory),
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">new</span>[] { <span style="color:#66d9ef">typeof</span>(IDungeonRoomFactory) },
</span></span><span style="display:flex;"><span>            Lifetime.Scoped,
</span></span><span style="display:flex;"><span>            Resolution.Lazy);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is not the only valid Reflex style. Some teams prefer <code>RegisterFactory</code> for bindings that need custom setup. Some wrap common registration calls in tiny helpers. The important part is not the exact registration ceremony. The important part is that the installer describes the scene graph in one place.</p>
<p>The room controller does not know any of this.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DungeonRoomController</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> RoomDefinition definition;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> IDungeonRoomFactory roomFactory = <span style="color:#66d9ef">null</span>!;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> DungeonRoomRuntime? runtime;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [Inject]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Construct(IDungeonRoomFactory roomFactory)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.roomFactory = roomFactory;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnEnable()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        runtime = roomFactory.Create(definition, transform);
</span></span><span style="display:flex;"><span>        runtime.Enter();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnDisable()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        runtime?.Dispose();
</span></span><span style="display:flex;"><span>        runtime = <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>MonoBehaviour</code> gets one dependency. That dependency says exactly what this object needs: a way to create room runtime objects. Not the whole container. Not every catalog. Not every service in the project.</p>
<h2 id="the-first-factory-create-one-room-runtime">The first factory: create one room runtime</h2>
<p>The room factory converts data plus runtime context into a runtime object.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IDungeonRoomFactory</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    DungeonRoomRuntime Create(RoomDefinition definition, Transform roomRoot);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The request is small on purpose. The factory receives the room definition and a root transform for spawned objects. Stable collaborators come from DI. Runtime values come from the method call.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DungeonRoomFactory</span> : IDungeonRoomFactory
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IRoomArchetypeFactorySelector archetypes;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IRandom random;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IDungeonAudio audio;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> DungeonRoomFactory(
</span></span><span style="display:flex;"><span>        IRoomArchetypeFactorySelector archetypes,
</span></span><span style="display:flex;"><span>        IRandom random,
</span></span><span style="display:flex;"><span>        IDungeonAudio audio)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.archetypes = archetypes;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.random = random;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.audio = audio;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> DungeonRoomRuntime Create(RoomDefinition definition, Transform roomRoot)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (definition == <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ArgumentNullException(nameof(definition));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> family = archetypes.Select(definition.Archetype);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> encounter = family.CreateEncounter(definition, roomRoot);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> rewards = family.CreateRewards(definition, roomRoot);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> hazards = family.CreateHazards(definition, roomRoot);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> hud = family.CreateHud(definition);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> DungeonRoomRuntime(
</span></span><span style="display:flex;"><span>            definition,
</span></span><span style="display:flex;"><span>            encounter,
</span></span><span style="display:flex;"><span>            rewards,
</span></span><span style="display:flex;"><span>            hazards,
</span></span><span style="display:flex;"><span>            hud,
</span></span><span style="display:flex;"><span>            random,
</span></span><span style="display:flex;"><span>            audio);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is where DI and factories meet cleanly.</p>
<p>The constructor contains dependencies that are stable for the lifetime of the factory: the archetype selector, random service, and audio service. The <code>Create</code> method contains values that change per room: the definition and root transform.</p>
<p>Do not pass <code>RoomDefinition</code> through the container. Do not register the current room root in Reflex just so another class can resolve it. That would turn per-room data into ambient state. The room definition is not a service. It is a request.</p>
<h2 id="the-abstract-factory-one-room-archetype-one-product-family">The abstract factory: one room archetype, one product family</h2>
<p>The previous article covered the reason abstract factories exist: they create related objects that must agree with each other. A dungeon room archetype is a useful example because a boss room, treasure room, and trap gauntlet do not just vary one object. They vary a family of related runtime pieces.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IRoomArchetypeFactory</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    RoomArchetype Archetype { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    IEncounterRuntime CreateEncounter(RoomDefinition definition, Transform root);
</span></span><span style="display:flex;"><span>    IRewardRuntime CreateRewards(RoomDefinition definition, Transform root);
</span></span><span style="display:flex;"><span>    IHazardRuntime CreateHazards(RoomDefinition definition, Transform root);
</span></span><span style="display:flex;"><span>    IRoomHudPresenter CreateHud(RoomDefinition definition);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A skirmish room and a treasure room can now produce different but consistent families.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SkirmishRoomFactory</span> : IRoomArchetypeFactory
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IEnemyFactory enemies;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ILootFactory loot;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ITrapFactory traps;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> DungeonHudView hudView;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> RoomArchetype Archetype =&gt; RoomArchetype.Skirmish;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> SkirmishRoomFactory(
</span></span><span style="display:flex;"><span>        IEnemyFactory enemies,
</span></span><span style="display:flex;"><span>        ILootFactory loot,
</span></span><span style="display:flex;"><span>        ITrapFactory traps,
</span></span><span style="display:flex;"><span>        DungeonHudView hudView)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.enemies = enemies;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.loot = loot;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.traps = traps;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.hudView = hudView;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IEncounterRuntime CreateEncounter(RoomDefinition definition, Transform root)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> WaveEncounterRuntime(enemies, definition.EnemyPack, root);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IRewardRuntime CreateRewards(RoomDefinition definition, Transform root)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> DeferredLootRuntime(loot, definition.LootTable, root);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IHazardRuntime CreateHazards(RoomDefinition definition, Transform root)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> EmptyHazardRuntime();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IRoomHudPresenter CreateHud(RoomDefinition definition)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> CombatRoomHudPresenter(hudView, definition.ThreatLevel);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A trap gauntlet uses the same product slots but different products.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">TrapGauntletRoomFactory</span> : IRoomArchetypeFactory
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ILootFactory loot;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ITrapFactory traps;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> DungeonHudView hudView;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> RoomArchetype Archetype =&gt; RoomArchetype.TrapGauntlet;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> TrapGauntletRoomFactory(
</span></span><span style="display:flex;"><span>        ILootFactory loot,
</span></span><span style="display:flex;"><span>        ITrapFactory traps,
</span></span><span style="display:flex;"><span>        DungeonHudView hudView)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.loot = loot;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.traps = traps;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.hudView = hudView;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IEncounterRuntime CreateEncounter(RoomDefinition definition, Transform root)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> EmptyEncounterRuntime();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IRewardRuntime CreateRewards(RoomDefinition definition, Transform root)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> ChestRewardRuntime(loot, definition.LootTable, root);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IHazardRuntime CreateHazards(RoomDefinition definition, Transform root)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> TrapLayoutRuntime(traps, definition.TrapLayout, root);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IRoomHudPresenter CreateHud(RoomDefinition definition)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> TrapRoomHudPresenter(hudView, definition.ThreatLevel);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The abstract factory boundary prevents mismatched room families. A trap gauntlet does not accidentally get a wave combat HUD unless that is intentionally encoded in its factory. The room runtime does not switch on archetype. It receives a complete set of matching pieces.</p>
<p>This is the point where an abstract factory earns its cost. The variation is not one class. It is a coordinated bundle.</p>
<h2 id="let-reflex-inject-the-factory-family">Let Reflex inject the factory family</h2>
<p>Reflex can resolve multiple bindings for the same contract through <code>All&lt;T&gt;()</code>. That is a good fit for a selector that maps each room archetype to its abstract factory.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IRoomArchetypeFactorySelector</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    IRoomArchetypeFactory Select(RoomArchetype archetype);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">RoomArchetypeFactorySelector</span> : IRoomArchetypeFactorySelector
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IReadOnlyDictionary&lt;RoomArchetype, IRoomArchetypeFactory&gt; factories;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> RoomArchetypeFactorySelector(IEnumerable&lt;IRoomArchetypeFactory&gt; factories)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.factories = factories.ToDictionary(factory =&gt; factory.Archetype);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IRoomArchetypeFactory Select(RoomArchetype archetype)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (factories.TryGetValue(archetype, <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> factory))
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> factory;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ArgumentException(<span style="color:#e6db74">$&#34;No room factory registered for {archetype}&#34;</span>, nameof(archetype));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The selector is intentionally dull. It turns a collection into a dictionary and fails loudly for missing archetypes. That failure matters. A missing room factory should not become an empty room, a default room, or a null reference three systems later.</p>
<p>The Reflex installer registers the concrete factories under the shared contract:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>builder.RegisterType(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">typeof</span>(SkirmishRoomFactory),
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">new</span>[] { <span style="color:#66d9ef">typeof</span>(IRoomArchetypeFactory) },
</span></span><span style="display:flex;"><span>    Lifetime.Scoped,
</span></span><span style="display:flex;"><span>    Resolution.Lazy);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>builder.RegisterType(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">typeof</span>(TrapGauntletRoomFactory),
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">new</span>[] { <span style="color:#66d9ef">typeof</span>(IRoomArchetypeFactory) },
</span></span><span style="display:flex;"><span>    Lifetime.Scoped,
</span></span><span style="display:flex;"><span>    Resolution.Lazy);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>builder.RegisterType(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">typeof</span>(RoomArchetypeFactorySelector),
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">new</span>[] { <span style="color:#66d9ef">typeof</span>(IRoomArchetypeFactorySelector) },
</span></span><span style="display:flex;"><span>    Lifetime.Scoped,
</span></span><span style="display:flex;"><span>    Resolution.Eager);
</span></span></code></pre></div><p>Eager resolution for the selector is a useful development choice. If a factory has a missing dependency, the scene fails while building the graph instead of during the first unlucky room. For production, the same rule still applies: fail as close to startup as practical for wiring mistakes.</p>
<p>The factories themselves can stay lazy. There is no reason to instantiate every room-family factory until something needs the selector, and the selector already forces that early if that is what the team wants.</p>
<h2 id="use-registerfactory-for-adapter-construction-not-gameplay-requests">Use <code>RegisterFactory</code> for adapter construction, not gameplay requests</h2>
<p>Reflex also has <code>RegisterFactory&lt;T&gt;(Func&lt;Container, T&gt; factory, ...)</code>. That is useful when a binding needs custom construction.</p>
<p>For example, maybe the enemy factory needs a prefab parent created from a scene object and a catalog from serialized data:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>builder.RegisterFactory&lt;IEnemyFactory&gt;(
</span></span><span style="display:flex;"><span>    container =&gt; <span style="color:#66d9ef">new</span> PrefabEnemyFactory(
</span></span><span style="display:flex;"><span>        container.Resolve&lt;EnemyCatalog&gt;(),
</span></span><span style="display:flex;"><span>        container.Resolve&lt;EnemyPool&gt;(),
</span></span><span style="display:flex;"><span>        enemyRoot),
</span></span><span style="display:flex;"><span>    Lifetime.Scoped,
</span></span><span style="display:flex;"><span>    Resolution.Lazy);
</span></span></code></pre></div><p>That is fine. The factory delegate is still part of the composition root. It builds a stable service: <code>IEnemyFactory</code>.</p>
<p>The mistake is registering per-room products this way:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#75715e">// Wrong shape for room-specific runtime data.</span>
</span></span><span style="display:flex;"><span>builder.RegisterFactory&lt;DungeonRoomRuntime&gt;(
</span></span><span style="display:flex;"><span>    container =&gt; <span style="color:#66d9ef">new</span> DungeonRoomRuntime(<span style="color:#75715e">/* which room definition? which root? */</span>),
</span></span><span style="display:flex;"><span>    Lifetime.Transient,
</span></span><span style="display:flex;"><span>    Resolution.Lazy);
</span></span></code></pre></div><p>The container does not know which room definition the player entered. If you force it to know, you will usually smuggle that data through mutable globals, temporary registrations, or a current-room service. That is container soup.</p>
<p>Per-request data belongs in a method argument:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>DungeonRoomRuntime Create(RoomDefinition definition, Transform roomRoot);
</span></span></code></pre></div><p>The DI container builds the factory. The factory handles the runtime request. Small distinction, large difference.</p>
<h2 id="the-product-factories-stay-narrow">The product factories stay narrow</h2>
<p>The abstract factory coordinates the family, but it should not contain every prefab lookup and spawn rule itself. That would turn it into the same giant switch with a nicer name.</p>
<p>Keep lower-level product factories narrow:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IEnemyFactory</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    EnemyRuntime Spawn(EnemyPackId packId, Transform root, <span style="color:#66d9ef">int</span> threatLevel);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ILootFactory</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    LootRuntime Create(LootTableId tableId, Transform root);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ITrapFactory</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    TrapRuntime CreateLayout(TrapLayoutId layoutId, Transform root);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Each one owns one kind of creation. The room-family factories compose them.</p>
<p>This gives each layer a reasonable job:</p>
<ul>
<li><code>PrefabEnemyFactory</code> knows enemy catalogs, pools, and initialization.</li>
<li><code>PrefabLootFactory</code> knows loot tables and pickup prefabs.</li>
<li><code>PrefabTrapFactory</code> knows trap layouts and arming rules.</li>
<li><code>SkirmishRoomFactory</code> knows which products a skirmish room needs.</li>
<li><code>DungeonRoomFactory</code> knows how to turn a room definition into one runtime.</li>
<li><code>DungeonRoomController</code> knows when the room starts and stops.</li>
</ul>
<p>No class is magical. Good. Magical code is where bugs breed.</p>
<h2 id="lifetime-rules-matter-more-than-pattern-names">Lifetime rules matter more than pattern names</h2>
<p>Factories make lifetime bugs easy to hide if the API is dishonest.</p>
<p>A room runtime is short-lived. It should be disposed or stopped when the room unloads. A factory may be scene-scoped. Catalogs may be scene-scoped or project-scoped. Pools may be scene-scoped because they own spawned Unity objects. Pure rules services may be singleton. These choices matter more than whether the class has <code>Factory</code> in the name.</p>
<p>A room runtime should make cleanup explicit:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DungeonRoomRuntime</span> : IDisposable
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IEncounterRuntime encounter;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IRewardRuntime rewards;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IHazardRuntime hazards;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IRoomHudPresenter hud;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IDungeonAudio audio;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> DungeonRoomRuntime(
</span></span><span style="display:flex;"><span>        RoomDefinition definition,
</span></span><span style="display:flex;"><span>        IEncounterRuntime encounter,
</span></span><span style="display:flex;"><span>        IRewardRuntime rewards,
</span></span><span style="display:flex;"><span>        IHazardRuntime hazards,
</span></span><span style="display:flex;"><span>        IRoomHudPresenter hud,
</span></span><span style="display:flex;"><span>        IRandom random,
</span></span><span style="display:flex;"><span>        IDungeonAudio audio)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.encounter = encounter;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.rewards = rewards;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.hazards = hazards;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.hud = hud;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.audio = audio;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Enter()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        hud.Show();
</span></span><span style="display:flex;"><span>        hazards.Arm();
</span></span><span style="display:flex;"><span>        encounter.Start();
</span></span><span style="display:flex;"><span>        audio.PlayRoomMusic();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Dispose()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        encounter.Dispose();
</span></span><span style="display:flex;"><span>        hazards.Dispose();
</span></span><span style="display:flex;"><span>        rewards.Dispose();
</span></span><span style="display:flex;"><span>        hud.Hide();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>If lower-level factories use pools, expose release behavior where needed. Do not pretend pooled objects are garbage-collected ordinary objects.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IEnemyFactory</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    EnemyRuntime Spawn(EnemyPackId packId, Transform root, <span style="color:#66d9ef">int</span> threatLevel);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Release(EnemyRuntime enemy);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Yes, <code>Release</code> makes the interface less cute. It also prevents hidden lifetime rules. Cute APIs do not get points when they leak enemies between rooms.</p>
<h2 id="testing-the-room-logic-without-reflex">Testing the room logic without Reflex</h2>
<p>One of the nicer outcomes of this split is that most tests do not need Reflex at all.</p>
<p>The room factory can be tested with fakes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[Test]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> BossRoomUsesBossFamily()
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> bossFamily = <span style="color:#66d9ef">new</span> FakeRoomArchetypeFactory(RoomArchetype.Boss);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> selector = <span style="color:#66d9ef">new</span> FakeSelector(bossFamily);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> factory = <span style="color:#66d9ef">new</span> DungeonRoomFactory(
</span></span><span style="display:flex;"><span>        selector,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> FixedRandom(),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> SilentDungeonAudio());
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> room = factory.Create(bossDefinition, fakeRoot);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Assert.That(bossFamily.CreatedEncounter, Is.True);
</span></span><span style="display:flex;"><span>    Assert.That(bossFamily.CreatedHud, Is.True);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A skirmish factory can be tested by passing fake product factories:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[Test]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> SkirmishRoomCreatesWaveEncounterFromEnemyPack()
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> enemies = <span style="color:#66d9ef">new</span> RecordingEnemyFactory();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> roomFactory = <span style="color:#66d9ef">new</span> SkirmishRoomFactory(
</span></span><span style="display:flex;"><span>        enemies,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> FakeLootFactory(),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> FakeTrapFactory(),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> FakeHudView());
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    roomFactory.CreateEncounter(skirmishDefinition, fakeRoot);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Assert.That(enemies.LastPackId, Is.EqualTo(skirmishDefinition.EnemyPack));
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The Reflex installer deserves a smaller integration check: build the scene container and verify the key contracts resolve. That check is about wiring. It should not test the whole dungeon.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[Test]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> DungeonInstallerRegistersRoomFactory()
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> builder = <span style="color:#66d9ef">new</span> ContainerBuilder();
</span></span><span style="display:flex;"><span>    installer.InstallBindings(builder);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">using</span> var container = builder.Build();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Assert.That(container.Resolve&lt;IDungeonRoomFactory&gt;(), Is.Not.Null);
</span></span><span style="display:flex;"><span>    Assert.That(container.All&lt;IRoomArchetypeFactory&gt;().Count(), Is.GreaterThan(<span style="color:#ae81ff">0</span>));
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That division keeps tests honest. Business logic tests use normal constructors. Container tests check container registration. Unity play mode tests check prefab and scene integration.</p>
<h2 id="common-mistakes-when-combining-reflex-and-factories">Common mistakes when combining Reflex and factories</h2>
<p>The first mistake is injecting <code>Container</code> everywhere.</p>
<p>Reflex lets code resolve from a container. That does not mean every runtime object should receive one. Passing the container into gameplay code gives that code unlimited access to the graph. It can now hide dependencies, resolve services at random times, and make tests harder than they need to be.</p>
<p>If a class needs audio, inject <code>IDungeonAudio</code>. If it needs a room factory, inject <code>IDungeonRoomFactory</code>. If it needs enemies, inject <code>IEnemyFactory</code>. Do not inject the toolbox when the class needs one wrench.</p>
<p>The second mistake is making every product an abstract factory product.</p>
<p>If all room archetypes use the same loot creation path, keep <code>ILootFactory</code> shared. The abstract factory can decide whether to create immediate loot, deferred loot, or no loot, but it does not need a separate loot factory type for every archetype unless the variation is real.</p>
<p>The third mistake is hiding runtime state inside singletons.</p>
<p>The current room definition, current room root, current threat level, and current player entry direction should not be global services. They are request data. Pass them through <code>Create</code> calls. If many values travel together, create a request object.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">struct</span> RoomCreationRequest(
</span></span><span style="display:flex;"><span>    RoomDefinition Definition,
</span></span><span style="display:flex;"><span>    Transform RoomRoot,
</span></span><span style="display:flex;"><span>    Vector3 EntryPoint,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> DungeonDepth);
</span></span></code></pre></div><p>That is clearer than sprinkling four temporary values through the container.</p>
<p>The fourth mistake is using abstract factories before the family exists.</p>
<p>If the only thing that changes between room types is enemy pack selection, use an enemy factory and data. Do not create <code>SkirmishRoomFactory</code>, <code>TreasureRoomFactory</code>, and <code>TrapRoomFactory</code> just to return the same three objects with one parameter changed. Data is cheaper than type hierarchies.</p>
<p>Abstract factory becomes useful when the family is real: encounter, rewards, hazards, and HUD all vary together.</p>
<h2 id="a-practical-rule-of-thumb">A practical rule of thumb</h2>
<p>Use Reflex for stable object graph construction:</p>
<ul>
<li>catalogs</li>
<li>pools</li>
<li>services</li>
<li>scene adapters</li>
<li>factories</li>
<li>selectors</li>
<li>long-lived presenters</li>
<li>configuration objects</li>
</ul>
<p>Use factories for runtime creation:</p>
<ul>
<li>enemies spawned during a room</li>
<li>loot created after victory</li>
<li>traps armed when the player enters</li>
<li>room runtimes built from definitions</li>
<li>HUD presenters that depend on room mode</li>
<li>temporary effects and gameplay objects</li>
</ul>
<p>Use abstract factories only when several products must stay consistent:</p>
<ul>
<li>boss room encounter, boss HUD, boss reward table, boss music cues</li>
<li>trap gauntlet hazards, warning UI, delayed rewards</li>
<li>treasure room empty encounter, chest rewards, relaxed HUD</li>
</ul>
<p>That last list is narrow on purpose. The abstract factory should describe a coherent room family, not the entire game.</p>
<h2 id="the-final-architecture">The final architecture</h2>
<p>The final shape is not complicated:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Scene Container
</span></span><span style="display:flex;"><span>  -&gt; stable services
</span></span><span style="display:flex;"><span>  -&gt; product factories
</span></span><span style="display:flex;"><span>  -&gt; room-archetype factories
</span></span><span style="display:flex;"><span>  -&gt; room factory
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>DungeonRoomController
</span></span><span style="display:flex;"><span>  -&gt; IDungeonRoomFactory.Create(definition, roomRoot)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>DungeonRoomRuntime
</span></span><span style="display:flex;"><span>  -&gt; encounter + rewards + hazards + HUD
</span></span></code></pre></div><p>The container is used at the edge. The factory receives runtime data. The abstract factory keeps related room products together. Product factories handle prefab and pool details. The room runtime owns behavior and cleanup.</p>
<p>That is the clean combination.</p>
<p>Reflex is not there to make gameplay code ask for whatever it wants whenever it wants. It is there to make the graph visible and injectable. Factories are not there to make object creation sound fancy. They are there to put creation policy in one small place. Abstract factories are not there for every branch. They are there when several created objects must belong to the same family.</p>
<p>The dungeon room example works because each tool has a boundary:</p>
<ul>
<li>Reflex wires the scene.</li>
<li><code>DungeonRoomFactory</code> creates one room runtime.</li>
<li><code>IRoomArchetypeFactory</code> creates a matching family for one archetype.</li>
<li>Enemy, loot, and trap factories handle lower-level products.</li>
<li>Runtime data stays in request arguments.</li>
<li>Cleanup stays explicit.</li>
</ul>
<p>That is the part worth copying into a real project. Not the exact class names. Not the number of interfaces. The boundary.</p>
<p>Start with direct construction. When creation starts spreading, introduce a small factory. When several related products must change together, introduce an abstract factory. When the factory needs stable collaborators, let Reflex inject them at the scene boundary. Then stop. The point is to keep room code readable, testable, and honest about what it creates. Anything beyond that is pattern collecting, and pattern collecting is how a simple dungeon room turns into a committee meeting with hit points.</p>
<p>Related reading: <a href="/posts/dependency-injection-in-unity/">Dependency Injection in Unity</a> covers the base DI rule for Unity code. <a href="/posts/dependency-injection-for-game-code/">Dependency Injection for Game Code</a> covers composition roots and wiring at the edges. <a href="/posts/factory-and-abstract-factory-patterns/">Factory and Abstract Factory Patterns</a> covers the factory pattern itself before Reflex enters the picture.</p>
]]></content>
        </item>
        
        <item>
            <title>Factory and Abstract Factory Patterns: Create the Thing Without Spreading the Choice Everywhere</title>
            <link>https://blog.jonmikeys.com/posts/factory-and-abstract-factory-patterns/</link>
            <pubDate>Sat, 05 Sep 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/factory-and-abstract-factory-patterns/</guid>
            <description>A practical guide to factory and abstract factory patterns in game code: when construction choices deserve a home, when a simple method is enough, and how to avoid turning creation code into architecture theater.</description>
            <content type="html"><![CDATA[<p>Factory patterns are useful when object creation has a decision hiding inside it.</p>
<p>That is the whole reason to care. Not because every <code>new</code> keyword needs a wrapper. Not because design pattern diagrams look tidy. A factory earns its place when code needs to choose which concrete thing to build, prepare it correctly, and keep that choice from leaking into every caller.</p>
<p>Game projects hit that shape constantly. A loot table creates different item instances from the same reward roll. A spawn system creates enemies from data. A UI flow creates screens from route names. A VFX system creates pooled effects from effect ids. A save loader creates versioned objects from older data. A network layer creates messages from packet types. The caller wants to say “give me the thing for this situation.” It should not need to know every concrete class, every prefab, every dependency, and every setup rule involved.</p>
<p>A factory gives creation a small home.</p>
<p>The trap is that “factory” sounds more important than it is. Most useful factories are boring. Many are one method. Some are just a dictionary from id to prefab. A few become full abstract factories. The work is deciding which size of factory fits the problem.</p>
<p><img src="/images/posts/factory-and-abstract-factory-patterns/diagram.svg" alt="Factory decision flow"></p>
<h2 id="the-small-version-a-factory-method">The small version: a factory method</h2>
<p>The simplest factory is a method that returns an object.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> Projectile CreateProjectile(ProjectileId id, Vector3 position, Quaternion rotation)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> prefab = projectileCatalog.GetPrefab(id);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> Object.Instantiate(prefab, position, rotation);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is already a factory. It does not need a suffix. It does not need an interface. It does not need a base class. It has one job: centralize the lookup and construction rule.</p>
<p>This is often enough for Unity code. If a weapon, enemy spawner, or UI controller needs to create something by id, the factory can hide where the prefab lives and how the object is initialized. The caller does not need to reach into addressables, resources, a serialized array, or a scene registry.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">EnemySpawner</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> EnemyFactory enemyFactory;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> EnemySpawner(EnemyFactory enemyFactory)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.enemyFactory = enemyFactory;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Enemy Spawn(EnemySpawnRequest request)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> enemy = enemyFactory.Create(request.EnemyId, request.Position);
</span></span><span style="display:flex;"><span>        enemy.SetLevel(request.Level);
</span></span><span style="display:flex;"><span>        enemy.SetPatrolRoute(request.PatrolRoute);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> enemy;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The spawner still owns spawn behavior. The factory owns creation. That boundary is small, which is why it works.</p>
<p>A good factory method usually answers three questions:</p>
<ol>
<li>Which concrete type or asset should be used?</li>
<li>Which dependencies or defaults must be applied?</li>
<li>Which setup rules should happen before the caller receives the object?</li>
</ol>
<p>If the method does not answer one of those questions, it may not need to exist.</p>
<h2 id="factories-are-not-anti-constructors">Factories are not anti-constructors</h2>
<p>The existence of factories does not make constructors bad.</p>
<p>A constructor is still the best place to require the dependencies and values an object must have before it can be valid.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DamagePopupPresenter</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> DamagePopupView view;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ColorPalette palette;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> DamagePopupPresenter(DamagePopupView view, ColorPalette palette)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.view = view;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.palette = palette;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That type does not need a factory just because it has a constructor. The constructor is clear. The required values are obvious. There is no hidden choice.</p>
<p>A factory starts to help when the construction process needs information the caller should not own.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> DamagePopupPresenter Create(DamageType damageType)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> view = viewPool.Get();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> palette = palettes.ForDamageType(damageType);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> DamagePopupPresenter(view, palette);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now the factory is buying something. It hides pooling, lookup, and palette selection. The caller only knows it wants a popup for a damage type.</p>
<p>This is the pattern at its healthiest: move creation policy out of gameplay code, but leave normal object invariants inside constructors.</p>
<h2 id="the-switch-statement-smell">The switch statement smell</h2>
<p>The common factory smell is a <code>switch</code> statement that appears in too many places.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">switch</span> (enemyType)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> EnemyType.Grunt:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> Object.Instantiate(gruntPrefab);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> EnemyType.Sniper:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> Object.Instantiate(sniperPrefab);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> EnemyType.Heavy:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> Object.Instantiate(heavyPrefab);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">default</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ArgumentOutOfRangeException(nameof(enemyType));
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This switch is not automatically bad. One switch in one factory is fine. A switch is often the simplest honest expression of a closed set of choices.</p>
<p>The problem starts when the same decision spreads:</p>
<ul>
<li>the spawn system switches on enemy type</li>
<li>the UI switches on enemy type to choose an icon</li>
<li>the audio system switches on enemy type to choose a bark set</li>
<li>the save loader switches on enemy type to restore runtime data</li>
<li>the tutorial system switches on enemy type to display help</li>
</ul>
<p>At that point, the enum is no longer just a value. It has become a distributed construction policy. Every new enemy type means hunting for branches across the codebase.</p>
<p>A factory can pull at least the creation part back into one place.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">EnemyFactory</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IReadOnlyDictionary&lt;EnemyType, EnemyDefinition&gt; definitions;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IServiceProvider services;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> EnemyFactory(
</span></span><span style="display:flex;"><span>        IReadOnlyDictionary&lt;EnemyType, EnemyDefinition&gt; definitions,
</span></span><span style="display:flex;"><span>        IServiceProvider services)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.definitions = definitions;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.services = services;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Enemy Create(EnemyType type, Vector3 position)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (!definitions.TryGetValue(type, <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> definition))
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ArgumentException(<span style="color:#e6db74">$&#34;Unknown enemy type: {type}&#34;</span>, nameof(type));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> enemy = Object.Instantiate(definition.Prefab, position, Quaternion.identity);
</span></span><span style="display:flex;"><span>        enemy.Initialize(definition.Stats, services.Get&lt;IAggroService&gt;());
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> enemy;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The caller no longer cares which prefab belongs to <code>EnemyType.Heavy</code>. It asks the factory. The factory becomes the single place where enemy creation policy lives.</p>
<p>Notice the ceiling, though. If every behavior still switches on enemy type somewhere else, a factory does not solve that. It only solves construction. Strategy, data-driven definitions, or polymorphism may be the right next step. Do not ask a factory to fix the whole architecture.</p>
<h2 id="factory-method-versus-factory-class">Factory Method versus Factory class</h2>
<p>The named Factory Method pattern is usually described with inheritance: a base class defines a creation method, and subclasses decide which concrete product to return.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">EncounterBuilder</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Encounter Build(Vector3 location)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> enemies = CreateEnemies(location);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> reward = CreateReward();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> Encounter(enemies, reward);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">abstract</span> IReadOnlyList&lt;Enemy&gt; CreateEnemies(Vector3 location);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">abstract</span> Reward CreateReward();
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That shape can be useful, but game code should not reach for inheritance first. Unity already brings enough inheritance and engine lifecycle into the room.</p>
<p>A plain factory class is often easier to test, easier to wire, and easier to replace.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IEncounterFactory</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    Encounter Create(EncounterDefinition definition, Vector3 location);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That interface is boring in the right way. The implementation can use prefabs, object pools, addressables, procedural generation, or test doubles. The caller does not care.</p>
<p>The lazy rule is simple: if the creation decision varies by runtime data, use a normal factory object. If the creation decision is naturally tied to a family of subclasses that already exists, Factory Method can fit. Do not create an inheritance tree just to make the pattern diagram happy.</p>
<h2 id="abstract-factory-families-not-one-object">Abstract factory: families, not one object</h2>
<p>An abstract factory creates a family of related objects that are meant to work together.</p>
<p>That phrase matters: <strong>family of related objects</strong>.</p>
<p>A single projectile factory is not an abstract factory. A UI theme factory that creates matching buttons, panels, tooltips, and icons can be. A platform factory that creates matching input prompts, save adapters, entitlement clients, and store links can be. A game mode factory that creates matching rules, scoring, HUD, and AI director can be.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IGameModeFactory</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    IScoreRules CreateScoreRules();
</span></span><span style="display:flex;"><span>    ISpawnDirector CreateSpawnDirector();
</span></span><span style="display:flex;"><span>    IMatchHud CreateHud();
</span></span><span style="display:flex;"><span>    IRewardTable CreateRewards();
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The point is not that each method is hard. The point is that the returned objects must agree with each other.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">TimeTrialFactory</span> : IGameModeFactory
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IScoreRules CreateScoreRules() =&gt; <span style="color:#66d9ef">new</span> TimeTrialScoreRules();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> ISpawnDirector CreateSpawnDirector() =&gt; <span style="color:#66d9ef">new</span> GhostSpawnDirector();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IMatchHud CreateHud() =&gt; <span style="color:#66d9ef">new</span> TimerHud();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IRewardTable CreateRewards() =&gt; <span style="color:#66d9ef">new</span> MedalRewardTable();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SurvivalFactory</span> : IGameModeFactory
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IScoreRules CreateScoreRules() =&gt; <span style="color:#66d9ef">new</span> SurvivalScoreRules();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> ISpawnDirector CreateSpawnDirector() =&gt; <span style="color:#66d9ef">new</span> WaveSpawnDirector();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IMatchHud CreateHud() =&gt; <span style="color:#66d9ef">new</span> WaveHud();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IRewardTable CreateRewards() =&gt; <span style="color:#66d9ef">new</span> LootCrateRewardTable();
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A match bootstrapper can now receive one factory and create a consistent set of systems.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MatchBootstrapper</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> MatchRuntime Build(IGameModeFactory factory)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> MatchRuntime(
</span></span><span style="display:flex;"><span>            factory.CreateScoreRules(),
</span></span><span style="display:flex;"><span>            factory.CreateSpawnDirector(),
</span></span><span style="display:flex;"><span>            factory.CreateHud(),
</span></span><span style="display:flex;"><span>            factory.CreateRewards());
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is the useful version of abstract factory. It prevents mismatched combinations. A survival HUD does not accidentally get paired with time-trial scoring because both came from the same family factory.</p>
<h2 id="where-abstract-factories-go-wrong">Where abstract factories go wrong</h2>
<p>Abstract factories become painful when the family is imaginary.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IEverythingFactory</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    Player CreatePlayer();
</span></span><span style="display:flex;"><span>    Enemy CreateEnemy();
</span></span><span style="display:flex;"><span>    Weapon CreateWeapon();
</span></span><span style="display:flex;"><span>    Quest CreateQuest();
</span></span><span style="display:flex;"><span>    Inventory CreateInventory();
</span></span><span style="display:flex;"><span>    SaveSystem CreateSaveSystem();
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is not a family. That is a service locator with a nicer coat.</p>
<p>The interface is too broad. It gives unrelated systems one shared dependency for no real reason. Any implementation now has to understand half the project, and tests that need one object may have to stub ten methods. That is not flexibility. That is a junk drawer.</p>
<p>A useful abstract factory should have a narrow theme:</p>
<ul>
<li>all products belong to one feature area</li>
<li>all products must be consistent with each other</li>
<li>callers usually need several products together</li>
<li>swapping the factory changes a coherent mode, platform, theme, or backend</li>
</ul>
<p>If those rules do not hold, use smaller factories.</p>
<h2 id="data-driven-factories-in-unity">Data-driven factories in Unity</h2>
<p>Unity factories often combine code with data assets.</p>
<p>A <code>ScriptableObject</code> catalog is a good fit when designers need to choose assets and tune values without editing code.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[CreateAssetMenu(menuName = &#34;Game/Enemy Catalog&#34;)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">EnemyCatalog</span> : ScriptableObject
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> EnemyDefinition[] enemies;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> Dictionary&lt;EnemyId, EnemyDefinition&gt;? lookup;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> EnemyDefinition Get(EnemyId id)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        lookup ??= enemies.ToDictionary(enemy =&gt; enemy.Id);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (!lookup.TryGetValue(id, <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> definition))
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ArgumentException(<span style="color:#e6db74">$&#34;Unknown enemy id: {id}&#34;</span>, nameof(id));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> definition;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then the factory uses the catalog.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">EnemyFactory</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> EnemyCatalog catalog;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> EnemyPool pool;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> EnemyFactory(EnemyCatalog catalog, EnemyPool pool)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.catalog = catalog;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.pool = pool;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Enemy Create(EnemyId id, Vector3 position)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> definition = catalog.Get(id);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> enemy = pool.Get(definition.Prefab);
</span></span><span style="display:flex;"><span>        enemy.transform.position = position;
</span></span><span style="display:flex;"><span>        enemy.Initialize(definition.Stats, definition.BehaviorProfile);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> enemy;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This keeps Unity-specific asset selection near the edge. Gameplay code can talk in ids and requests instead of serialized arrays and prefab references.</p>
<p>The important detail is validation. A data-driven factory should fail loudly when data is missing, duplicated, or invalid. Silent fallback enemies and null prefabs are how production bugs become ghost stories.</p>
<p>A small editor validation pass or <code>OnValidate</code> check is usually enough:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnValidate()
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> seen = <span style="color:#66d9ef">new</span> HashSet&lt;EnemyId&gt;();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">foreach</span> (<span style="color:#66d9ef">var</span> enemy <span style="color:#66d9ef">in</span> enemies)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (enemy.Prefab == <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>            Debug.LogError(<span style="color:#e6db74">$&#34;Enemy {enemy.Id} has no prefab&#34;</span>, <span style="color:#66d9ef">this</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (!seen.Add(enemy.Id))
</span></span><span style="display:flex;"><span>            Debug.LogError(<span style="color:#e6db74">$&#34;Duplicate enemy id: {enemy.Id}&#34;</span>, <span style="color:#66d9ef">this</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The factory should be boring at runtime because the data was checked before runtime.</p>
<h2 id="pools-are-factories-with-memory">Pools are factories with memory</h2>
<p>Object pools and factories often meet, but they are not the same thing.</p>
<p>A factory decides how to create or acquire an object. A pool decides how to reuse object instances. In Unity, a factory may use a pool internally because instantiating prefabs during gameplay can be expensive.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> Projectile Create(ProjectileRequest request)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> projectile = projectilePool.Get(request.ProjectileId);
</span></span><span style="display:flex;"><span>    projectile.transform.SetPositionAndRotation(request.Origin, request.Rotation);
</span></span><span style="display:flex;"><span>    projectile.Initialize(request.Owner, request.Damage, request.Speed);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> projectile;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The caller should not need to know whether the projectile came from <code>Instantiate</code>, an addressable load, or a warm pool. That is creation policy.</p>
<p>But the factory should not hide object lifetime rules. If callers must return objects to a pool, make that contract visible.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IProjectileFactory</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    Projectile Create(ProjectileRequest request);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Release(Projectile projectile);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is less pretty than a one-way <code>Create</code> method, but it is honest. Lifetime is part of the API. Hiding it creates leaks, double releases, and spooky reuse bugs.</p>
<h2 id="factories-and-dependency-injection">Factories and dependency injection</h2>
<p>Factories pair well with dependency injection when objects are created after startup.</p>
<p>A composition root can build long-lived services at the edge of the program. That works for systems that exist for the lifetime of a scene or match. It does not solve runtime creation by itself. Enemies, bullets, popups, commands, and temporary view models are created later.</p>
<p>That is where a factory is useful: inject the factory into the system that needs runtime objects.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Weapon</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IProjectileFactory projectileFactory;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Weapon(IProjectileFactory projectileFactory)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.projectileFactory = projectileFactory;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Fire(FireRequest request)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        projectileFactory.Create(<span style="color:#66d9ef">new</span> ProjectileRequest(
</span></span><span style="display:flex;"><span>            request.ProjectileId,
</span></span><span style="display:flex;"><span>            request.Origin,
</span></span><span style="display:flex;"><span>            request.Rotation,
</span></span><span style="display:flex;"><span>            request.Owner));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The weapon is not responsible for prefab lookup, pooling, addressables, or projectile dependencies. It only knows how to ask for a projectile.</p>
<p>This is also where service locators sneak in. A factory that reaches into a global container for everything can become hard to reason about.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#75715e">// Usually a smell inside runtime code.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> audio = ServiceLocator.Get&lt;IAudioService&gt;();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> stats = ServiceLocator.Get&lt;IStatsService&gt;();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> pool = ServiceLocator.Get&lt;IProjectilePool&gt;();
</span></span></code></pre></div><p>Prefer giving the factory its dependencies through its constructor. If the factory needs a pool, catalog, random source, or clock, make that visible when the factory is built. Runtime callers should pass runtime data, not global access requests.</p>
<h2 id="a-practical-decision-guide">A practical decision guide</h2>
<p>Factories are easiest to size by starting with the smallest thing that could work.</p>
<p>Use a constructor when:</p>
<ul>
<li>the type has fixed required dependencies</li>
<li>there is no concrete-type selection</li>
<li>setup is simple and local</li>
<li>the caller already has the values it needs</li>
</ul>
<p>Use a static helper or simple method when:</p>
<ul>
<li>construction is mostly mechanical</li>
<li>there is one obvious output type</li>
<li>the helper has no meaningful state</li>
<li>test replacement is not important</li>
</ul>
<p>Use a factory class when:</p>
<ul>
<li>creation depends on runtime data</li>
<li>construction uses catalogs, pools, prefabs, or external services</li>
<li>several callers need the same creation rule</li>
<li>invalid creation should be validated in one place</li>
<li>tests need to replace creation with a fake</li>
</ul>
<p>Use an abstract factory when:</p>
<ul>
<li>several related products must be created together</li>
<li>those products must belong to the same family</li>
<li>swapping the factory swaps a coherent mode, platform, theme, or backend</li>
<li>the caller should not mix products from different families</li>
</ul>
<p>Do not use a factory when:</p>
<ul>
<li>it only wraps <code>new</code> with no added policy</li>
<li>the name exists only to satisfy a pattern checklist</li>
<li>every method returns an unrelated system</li>
<li>a data table or object initializer would be clearer</li>
<li>the factory hides lifetime rules that callers still need to respect</li>
</ul>
<p>The factory should make construction easier to trust. If it only makes construction harder to find, it failed.</p>
<h2 id="the-testability-angle">The testability angle</h2>
<p>Factories are useful in tests because creation is a side effect.</p>
<p>Instantiating Unity prefabs, loading assets, asking addressables for data, or touching pools can make unit tests heavy. A small factory interface lets gameplay code be tested without dragging the engine into every check.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">FakeProjectileFactory</span> : IProjectileFactory
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">readonly</span> List&lt;ProjectileRequest&gt; Requests = <span style="color:#66d9ef">new</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Projectile Create(ProjectileRequest request)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Requests.Add(request);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">null</span>!;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Release(Projectile projectile)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now a weapon test can verify intent:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>weapon.Fire(request);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Assert.That(fakeFactory.Requests, Has.Count.EqualTo(<span style="color:#ae81ff">1</span>));
</span></span><span style="display:flex;"><span>Assert.That(fakeFactory.Requests[<span style="color:#ae81ff">0</span>].ProjectileId, Is.EqualTo(expectedId));
</span></span></code></pre></div><p>That test does not prove the projectile prefab works. It proves the weapon asks for the right thing. The factory itself can have a smaller integration test that verifies catalogs, prefabs, pools, and initialization.</p>
<p>That separation is valuable. The game system test stays fast. The Unity integration test checks the Unity edge.</p>
<h2 id="keep-the-boundary-honest">Keep the boundary honest</h2>
<p>A factory sits at a boundary. That means it should be explicit about what crosses the boundary.</p>
<p>Good inputs are usually request objects, ids, definitions, positions, owners, or mode choices.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">struct</span> EnemySpawnRequest(
</span></span><span style="display:flex;"><span>    EnemyId EnemyId,
</span></span><span style="display:flex;"><span>    Vector3 Position,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> Level,
</span></span><span style="display:flex;"><span>    PatrolRoute? PatrolRoute);
</span></span></code></pre></div><p>Bad inputs are often giant bags of unrelated state.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> Enemy Create(GameManager manager)
</span></span></code></pre></div><p>That factory can now pull anything from anywhere. It is flexible in the same way a junk drawer is flexible. The caller no longer knows what creation depends on, and the factory can grow invisible coupling forever.</p>
<p>A factory request should say what the new object needs, not hand over the whole game.</p>
<p>The same applies to outputs. Return the narrowest useful type. If callers only need an interface, return the interface. If Unity code needs the concrete component for setup, return the component. Do not force abstraction where the engine boundary needs a concrete object.</p>
<h2 id="the-boring-final-shape">The boring final shape</h2>
<p>A good factory is not dramatic. It is a named seam around creation policy.</p>
<p>It keeps prefab lookup out of gameplay logic. It keeps data validation near the catalog. It keeps object pooling behind a clear API. It keeps runtime object creation testable. It keeps abstract families consistent when several related systems need to be swapped together.</p>
<p>The pattern is worth using when construction has real rules. It is not worth using when construction is already obvious.</p>
<p>That is the lazy version: create the thing directly until the choice starts spreading. Then put the choice in one small place. If several related choices must move together, make that place an abstract factory. Stop there. The goal is not to collect patterns. The goal is to keep creation code from leaking all over the game.</p>
<p>Related reading: <a href="/posts/reflex-di-factories-abstract-factories/">Combining Reflex DI with Factories</a> continues this idea with a concrete Unity dungeon-room example using Reflex as the DI container. <a href="/posts/builder-fluent-builder-patterns/">Builder and Fluent Builder Patterns</a> covers complex object setup after the creation choice has already been made. <a href="/posts/strategy-pattern-in-games/">Strategy Pattern in Games</a> covers swapping behavior after an object exists. <a href="/posts/dependency-injection-for-game-code/">Dependency Injection for Game Code</a> covers keeping long-lived wiring at the edges instead of letting runtime code hunt for dependencies.</p>
]]></content>
        </item>
        
        <item>
            <title>Strategy Pattern in Games: Swap Behavior Without Growing a Branch Monster</title>
            <link>https://blog.jonmikeys.com/posts/strategy-pattern-in-games/</link>
            <pubDate>Sat, 05 Sep 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/strategy-pattern-in-games/</guid>
            <description>A practical guide to using the strategy pattern in game code for weapons, movement, AI decisions, scoring rules, and other behavior that needs to change safely.</description>
            <content type="html"><![CDATA[<p>The strategy pattern is what I reach for when a game system has one job, but several valid ways to do it.</p>
<p>A weapon can calculate damage in different ways. An enemy can choose movement differently depending on its type. A racing game can score drift, time trial, and checkpoint modes with different rules. A character controller can use grounded, swimming, climbing, and flying movement. The surrounding system wants to say “do the thing,” but it should not need a <code>switch</code> statement that knows every possible version of the thing.</p>
<p>That is the small promise of the strategy pattern: put interchangeable behavior behind a shared contract, then let the caller use the contract instead of hard-coding every branch.</p>
<p>It is not a license to turn every <code>if</code> into an interface. Games already have enough indirection from scenes, prefabs, assets, and engine callbacks. Strategy is useful when the variation is real, repeated, and likely to grow. If the code has two simple branches and nobody expects a third, keep the branch. Boring code wins until boring code starts fighting back.</p>
<p><img src="/images/posts/strategy-pattern-in-games/diagram.svg" alt="Strategy pattern flow"></p>
<h2 id="the-shape-of-the-pattern">The shape of the pattern</h2>
<p>A strategy has three parts.</p>
<p>The <strong>context</strong> is the object that needs behavior. A weapon, enemy brain, movement controller, damage resolver, quest reward system, or scoring manager can all be contexts.</p>
<p>The <strong>strategy interface</strong> describes the behavior the context needs. It should be small. One method is common. Two or three can be fine if they belong together. If the interface starts describing half the game, it is no longer a strategy; it is a subsystem wearing a pattern hat.</p>
<p>The <strong>concrete strategies</strong> are the different implementations. Each one handles one version of the behavior.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IDamageStrategy</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> CalculateDamage(DamageRequest request);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">FlatDamageStrategy</span> : IDamageStrategy
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> CalculateDamage(DamageRequest request)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> request.BaseDamage;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CriticalHitStrategy</span> : IDamageStrategy
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">float</span> critChance;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">float</span> critMultiplier;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IRandom random;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> CriticalHitStrategy(<span style="color:#66d9ef">float</span> critChance, <span style="color:#66d9ef">float</span> critMultiplier, IRandom random)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.critChance = critChance;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.critMultiplier = critMultiplier;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.random = random;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> CalculateDamage(DamageRequest request)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> damage = request.BaseDamage;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (random.Value01() &lt; critChance)
</span></span><span style="display:flex;"><span>            damage = (<span style="color:#66d9ef">int</span>)(damage * critMultiplier);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> damage;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">struct</span> DamageRequest(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> BaseDamage,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> AttackerLevel,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> DefenderArmor);
</span></span></code></pre></div><p>The caller does not need to know whether the damage was flat, critical, elemental, armor-piercing, or difficulty-scaled. It only needs <code>CalculateDamage</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Weapon</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IDamageStrategy damageStrategy;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Weapon(IDamageStrategy damageStrategy)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.damageStrategy = damageStrategy;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Hit(Target target, DamageRequest request)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        target.ApplyDamage(damageStrategy.CalculateDamage(request));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is the pattern. Put the changing algorithm behind a stable call site.</p>
<p>The trick, as usual, is deciding where the stable call site actually is.</p>
<h2 id="start-with-the-branch-you-are-trying-to-remove">Start with the branch you are trying to remove</h2>
<p>Strategy usually begins life as a branch.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> CalculateDamage(WeaponType type, DamageRequest request)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">switch</span> (type)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">case</span> WeaponType.Sword:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> request.BaseDamage;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">case</span> WeaponType.Dagger:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> RollCrit(request.BaseDamage);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">case</span> WeaponType.Hammer:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> ApplyArmorBreak(request.BaseDamage, request.DefenderArmor);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">default</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ArgumentOutOfRangeException(nameof(type));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is not automatically bad. A <code>switch</code> can be the clearest code in the room. It is local, visible, easy to debug, and cheap to step through. I would rather maintain a five-case switch that fits on one screen than chase five classes that exist only because someone wanted to use a pattern.</p>
<p>The smell appears when the same decision leaks into several places.</p>
<p>Maybe damage calculation switches on <code>WeaponType</code>. Then VFX selection switches on <code>WeaponType</code>. Then tooltip text switches on <code>WeaponType</code>. Then AI weapon preference switches on <code>WeaponType</code>. Now adding a hammer means editing four unrelated files and hoping every switch was updated.</p>
<p>That is when strategy starts to earn its keep. It moves the variation into one replaceable object, so the rest of the system can depend on behavior instead of category checks.</p>
<p>A useful rule: do not extract a strategy because a branch exists. Extract a strategy because the branch represents a policy that changes independently from the caller.</p>
<h2 id="good-game-code-candidates">Good game-code candidates</h2>
<p>Damage is the easy example, but strategy shows up in many ordinary game systems.</p>
<p>Movement is a strong candidate. A character might use different movement strategies for walking, swimming, climbing, knockback, ladders, rails, or vehicles. The controller still owns input, collision integration, animation parameters, and lifecycle. The movement strategy owns how desired movement becomes velocity for the current mode.</p>
<p>AI decisions are another common fit. A shooter enemy might choose between aggressive, defensive, patrol, and flee strategies. A tactics unit might choose a target using nearest enemy, lowest health, highest threat, or objective pressure. The blackboard and perception system can stay shared while the selection policy changes.</p>
<p>Spawning rules often benefit from the same idea. A wave director can ask a spawn strategy for the next group: fixed sequence, budget-based random, difficulty curve, biome-filtered, or director-driven catch-up. The director should not become a 900-line random table parser just because level design needs several modes.</p>
<p>Scoring and rewards are also good candidates. Arcade score, combo score, stealth score, time-trial medals, daily challenge modifiers, and assist-mode adjustments are all policies. Keeping them behind a small strategy makes it easier to test and easier to explain to designers.</p>
<p>Serialization and save migration can use strategy too, though I would be careful there. Version-specific migration strategies can keep old save handling isolated. The danger is hiding data-loss behavior behind too much indirection. If a strategy touches player saves, the contract needs validation, logging, and tests. Lazy code does not mean casual with player data.</p>
<h2 id="keep-the-strategy-interface-narrow">Keep the strategy interface narrow</h2>
<p>Most strategy implementations rot because the interface is too wide.</p>
<p>The context starts with one need:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>Vector3 GetDesiredVelocity(MoveInput input, MoveState state);
</span></span></code></pre></div><p>Then someone adds animation. Then footstep audio. Then stamina. Then camera bob. Soon the interface has ten methods and every implementation contains empty stubs.</p>
<p>That is a warning. A strategy should represent one axis of variation. Walking, swimming, and flying movement can reasonably share velocity calculation. They probably should not also own UI prompts, inventory restrictions, and camera logic unless those things genuinely vary as part of the same policy.</p>
<p>When an interface grows, split the reason for change before adding more methods. Movement calculation can be one strategy. Animation presentation can be another system that reads movement state. Stamina cost can be data on the movement mode or a separate cost policy. The exact split depends on the project, but the question is consistent: who needs to change together?</p>
<p>A narrow interface also makes testing less painful. A one-method strategy can be tested with a few input cases. A strategy that requires a scene, animator, camera, audio mixer, inventory, and network connection is not a strategy anymore. It is a feature module, and it should be treated like one.</p>
<h2 id="unity-gives-you-two-common-implementation-styles">Unity gives you two common implementation styles</h2>
<p>In plain C#, a strategy is often just an interface and a class. In Unity, there are two common ways to wire that into real projects.</p>
<p>The first is runtime composition with normal C# objects. This works well for systems created by code: combat resolvers, simulation models, server-authoritative gameplay, testable domain logic, or tools that do not need every policy exposed in the Inspector.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> weapon = <span style="color:#66d9ef">new</span> Weapon(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">new</span> CriticalHitStrategy(<span style="color:#ae81ff">0.15f</span>, <span style="color:#ae81ff">2.0f</span>, random));
</span></span></code></pre></div><p>This style is explicit and easy to test. It also avoids pretending that every gameplay decision needs to be a Unity asset. For pure logic, plain C# is often the cleanest option.</p>
<p>The second style is ScriptableObject strategies. This is useful when designers need to select or tune behavior in the editor.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DamageStrategyAsset</span> : ScriptableObject
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">int</span> CalculateDamage(DamageRequest request);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">[CreateAssetMenu(menuName = &#34;Combat/Damage/Flat Damage&#34;)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">FlatDamageStrategyAsset</span> : DamageStrategyAsset
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">override</span> <span style="color:#66d9ef">int</span> CalculateDamage(DamageRequest request)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> request.BaseDamage;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">[CreateAssetMenu(menuName = &#34;Combat/Damage/Armor Piercing&#34;)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ArmorPiercingDamageAsset</span> : DamageStrategyAsset
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">float</span> armorIgnored = <span style="color:#ae81ff">0.5f</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">override</span> <span style="color:#66d9ef">int</span> CalculateDamage(DamageRequest request)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> effectiveArmor = request.DefenderArmor * (<span style="color:#ae81ff">1f</span> - armorIgnored);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> Math.Max(<span style="color:#ae81ff">1</span>, request.BaseDamage - (<span style="color:#66d9ef">int</span>)effectiveArmor);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then a weapon definition can reference the asset.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">WeaponDefinition</span> : ScriptableObject
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> DamageStrategyAsset damageStrategy;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> CalculateDamage(DamageRequest request)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> damageStrategy.CalculateDamage(request);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This can be a good Unity workflow. Designers can create assets, tune values, and reuse behavior across many definitions. The strategy asset becomes a small data-backed policy.</p>
<p>The catch is state. ScriptableObjects are assets, not per-enemy scratchpads. A ScriptableObject strategy should generally be stateless or treat serialized fields as shared configuration. If a strategy needs per-instance runtime state, keep that state in the context or create a runtime strategy instance from the asset.</p>
<p>Shared mutable state in an asset is one of those bugs that looks fine with one enemy and turns weird with fifty.</p>
<h2 id="do-not-use-strategy-to-hide-bad-ownership">Do not use strategy to hide bad ownership</h2>
<p>Strategy can clean up variation. It cannot fix unclear ownership.</p>
<p>If a weapon strategy needs to change the inventory, camera, quest log, audio mixer, player stats, and UI, the problem is not that the strategy needs more dependencies. The problem is that the behavior is crossing too many boundaries.</p>
<p>Sometimes the right answer is an event. The weapon applies damage, publishes a hit event, and lets audio or UI respond. Sometimes the right answer is a command object. The strategy decides what command should happen, and a separate executor applies it. Sometimes the right answer is to keep the behavior in the owning system and pass in data, not services.</p>
<p>A clean strategy usually answers a focused question:</p>
<ul>
<li>How much damage should this hit do?</li>
<li>Which target should this unit prefer?</li>
<li>Which spawn group should come next?</li>
<li>What velocity should this movement mode request?</li>
<li>How should this score be converted into a medal?</li>
</ul>
<p>A messy strategy tries to run the game from the side.</p>
<p>That distinction matters because indirection has a cost. Not usually a frame-time cost, though virtual calls in hot loops can matter in data-oriented code. The bigger cost is comprehension. A programmer debugging a weapon hit now has to find which strategy is assigned, which asset configured it, which prefab references it, and whether play mode state modified it. That is fine when the behavior is truly variable. It is annoying when there was only one behavior and a pattern got there first.</p>
<h2 id="a-practical-refactor-path">A practical refactor path</h2>
<p>The safest way to introduce strategy is not to redesign the whole system. Extract one policy.</p>
<p>Start by finding the repeated branch or bloated method. Pick the smallest behavior that changes independently. Define a contract for that behavior only. Move one existing case behind the contract, then move the other cases. Keep the caller almost unchanged.</p>
<p>Before:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> SpawnGroup ChooseNextGroup(Biome biome, <span style="color:#66d9ef">int</span> danger, <span style="color:#66d9ef">bool</span> bossAlive)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (bossAlive)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> bossAddsTable.Roll(danger);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (biome == Biome.Swamp)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> swampTable.Roll(danger);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (danger &gt; <span style="color:#ae81ff">80</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> eliteTable.Roll(danger);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> defaultTable.Roll(danger);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>After:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ISpawnStrategy</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    SpawnGroup ChooseNextGroup(SpawnContext context);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BiomeSpawnStrategy</span> : ISpawnStrategy
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> SpawnGroup ChooseNextGroup(SpawnContext context)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (context.BossAlive)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> context.Tables.BossAdds.Roll(context.Danger);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (context.Biome == Biome.Swamp)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> context.Tables.Swamp.Roll(context.Danger);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (context.Danger &gt; <span style="color:#ae81ff">80</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> context.Tables.Elite.Roll(context.Danger);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> context.Tables.Default.Roll(context.Danger);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That first extraction may still contain branches, and that is fine. The goal is not branch elimination. The goal is placing the policy behind a seam so a different mode can replace it later.</p>
<p>For example, a daily challenge can use a different strategy without editing the normal campaign logic.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DailyChallengeSpawnStrategy</span> : ISpawnStrategy
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> SpawnGroup ChooseNextGroup(SpawnContext context)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> context.Tables.DailyChallenge.RollSeeded(
</span></span><span style="display:flex;"><span>            context.RunSeed,
</span></span><span style="display:flex;"><span>            context.WaveIndex);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now the wave director depends on <code>ISpawnStrategy</code>, not on every possible spawn rule.</p>
<p>That is enough. Stop there unless the next problem demands more.</p>
<h2 id="testing-strategies-without-dragging-unity-into-it">Testing strategies without dragging Unity into it</h2>
<p>One of the best reasons to use strategy is that it gives you small pieces of behavior you can test without booting a scene.</p>
<p>For deterministic strategies, use plain inputs and expected outputs.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ArmorPiercingDamageTests</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">void</span> Run()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> strategy = <span style="color:#66d9ef">new</span> ArmorPiercingDamage(armorIgnored: <span style="color:#ae81ff">0.5f</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> damage = strategy.CalculateDamage(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">new</span> DamageRequest(BaseDamage: <span style="color:#ae81ff">30</span>, AttackerLevel: <span style="color:#ae81ff">1</span>, DefenderArmor: <span style="color:#ae81ff">10</span>));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        Debug.Assert(damage == <span style="color:#ae81ff">25</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>For random strategies, inject randomness. Do not call <code>UnityEngine.Random.value</code> directly from the strategy if you want repeatable tests. Pass an <code>IRandom</code>, a seeded random source, or a precomputed roll value in the request. This is not ceremony. It is the difference between a test that catches a balance bug and a test that occasionally fails because the moon looked at it wrong.</p>
<p>For ScriptableObject strategies, keep the calculation method as pure as practical. If the asset only reads serialized configuration and a request struct, it is still easy to test. If the asset searches the scene, pulls singletons, and mutates global state, the strategy is now coupled to the runtime and the tests will pay for it.</p>
<p>A small amount of discipline here saves a lot of editor-time debugging later.</p>
<h2 id="performance-notes">Performance notes</h2>
<p>Most strategy use in game architecture is not performance-sensitive. A virtual call when a weapon hits or a wave spawns is noise. The design clarity matters more.</p>
<p>Hot loops are different. If you are running behavior selection for tens of thousands of entities every frame, classic object-oriented strategy may fight the CPU. Interface dispatch, scattered objects, and asset lookups can hurt cache behavior. In those cases, a data-oriented representation is often better: enums plus Burst-friendly function selection, lookup tables, or separate jobs per behavior type.</p>
<p>That does not make strategy wrong. It means the implementation should match the layer.</p>
<p>At authoring and gameplay-system scale, strategy objects are often fine. At simulation-kernel scale, prefer data layouts that the runtime can chew through efficiently. Same concept, different mechanics.</p>
<p>The lazy version is not “always use interfaces.” It is “use the smallest representation that keeps the code honest at this scale.”</p>
<h2 id="common-mistakes">Common mistakes</h2>
<p>The first mistake is making a strategy for every tiny variation. If each strategy is a two-line class and the caller was simpler before, you probably extracted too early. Wait until there is a real seam.</p>
<p>The second mistake is letting strategies know too much. A damage strategy should not need the entire player object if a <code>DamageRequest</code> with level, armor, tags, and buffs would do. Passing smaller data makes the contract clearer and reduces accidental dependencies.</p>
<p>The third mistake is using inheritance when composition would be simpler. A <code>FireSword : Sword : Weapon : MonoBehaviour</code> hierarchy grows brittle quickly. A weapon with a damage strategy, targeting strategy, and presentation data can often handle variation without forcing every combination into a subclass.</p>
<p>The fourth mistake is hiding assignment. If a prefab gets the wrong strategy asset, debugging should be straightforward. Use clear asset names, validate required references, and fail loudly when a strategy is missing. A null strategy should not silently become “do no damage” unless that is an intentional, named behavior.</p>
<p>The fifth mistake is pretending strategy replaces design. You still have to decide which behaviors are allowed, how they combine, and what owns the result. Patterns organize decisions. They do not make the decisions for you.</p>
<h2 id="when-a-switch-is-still-better">When a switch is still better</h2>
<p>There are plenty of times when I would keep the switch.</p>
<p>If the behavior is tiny and closed, a switch is clearer. If the cases are tightly coupled and always change together, a switch keeps that relationship visible. If the code is in a Burst job or another hot path where object dispatch is the wrong shape, data-oriented branching may be the correct tool. If the variation is only in values, use data. Do not create <code>FastEnemyMovementStrategy</code>, <code>MediumEnemyMovementStrategy</code>, and <code>SlowEnemyMovementStrategy</code> when a speed field would do.</p>
<p>This is the part that gets missed in pattern discussions. The strategy pattern is not about avoiding <code>if</code>. It is about separating policies that need to vary independently.</p>
<p>A branch answers “which case is this?”
A strategy answers “which behavior did we plug in?”</p>
<p>Both are useful. Pick the one that makes the next change cheaper, not the one that looks more architectural.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Use the strategy pattern when a game system has a stable job but multiple real policies for doing that job.</p>
<p>Keep the interface narrow. Pass data instead of whole objects. Use plain C# strategies for runtime logic and ScriptableObject strategies when editor-driven authoring matters. Avoid shared mutable state in assets. Do not extract a strategy just to remove a branch. Extract it when the branch is a policy that needs to change, be tested, or be selected independently.</p>
<p>The best strategy implementation is usually small enough to look boring. That is the point. The caller says what it needs. The strategy decides how to do it. The rest of the game stays out of the argument.</p>
<h2 id="references">References</h2>
<ul>
<li>Microsoft Learn: <a href="https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/interfaces">Interfaces - define behavior for multiple types</a></li>
<li>Unity Manual: <a href="https://docs.unity3d.com/Manual/class-ScriptableObject.html">ScriptableObject</a></li>
<li>Game Programming Patterns: <a href="https://gameprogrammingpatterns.com/state.html">State</a> — useful adjacent reading for comparing behavior swapping with state-driven mode changes.</li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Memento Pattern in Games: Save the State Without Exposing the Guts</title>
            <link>https://blog.jonmikeys.com/posts/memento-pattern-game-state-snapshots/</link>
            <pubDate>Tue, 01 Sep 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/memento-pattern-game-state-snapshots/</guid>
            <description>A practical guide to using the memento pattern for undo, checkpoints, editor tools, and save snapshots in game code without leaking internal state everywhere.</description>
            <content type="html"><![CDATA[<p>The memento pattern is useful when a system needs to go back to a previous state, but nobody outside that system should be allowed to rummage through its internals.</p>
<p>That sounds like a save system, and it can be. It also sounds like undo in an editor tool, rollback for a turn-based tactics prototype, checkpoints in a puzzle game, temporary simulation branches for AI planning, or a “try this change and cancel if it fails” workflow in gameplay code. All of those problems have the same shape: capture enough state to restore later, keep the capture owned by the object that understands it, and avoid turning private fields into public plumbing.</p>
<p>The pattern is not glamorous. That is why I like it. Good mementos are boring containers at the edge of a system. They make restoration explicit without pretending that every object in the game can serialize itself safely just because the compiler allowed it.</p>
<p><img src="/images/posts/memento-pattern-game-state-snapshots/diagram.svg" alt="Memento snapshot flow"></p>
<h2 id="the-small-version">The small version</h2>
<p>The memento pattern has three roles.</p>
<p>The <strong>originator</strong> is the object whose state can be captured and restored. The <strong>memento</strong> is the snapshot. The <strong>caretaker</strong> stores the snapshot and gives it back later, but does not understand or modify what is inside it.</p>
<p>In game terms, the originator might be a puzzle board, an inventory model, a unit stat block, a dialogue graph editor, or a combat command queue. The caretaker might be an undo stack, save slot manager, checkpoint trigger, or test harness. The important rule is that the caretaker does not get to manipulate the originator’s private details directly.</p>
<p>A small C# shape looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Inventory</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> List&lt;ItemStack&gt; items = <span style="color:#66d9ef">new</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> InventorySnapshot CreateSnapshot()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> InventorySnapshot(items.Select(x =&gt; x.Clone()).ToArray());
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Restore(InventorySnapshot snapshot)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        items.Clear();
</span></span><span style="display:flex;"><span>        items.AddRange(snapshot.Items.Select(x =&gt; x.Clone()));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">InventorySnapshot</span>(ItemStack[] Items);
</span></span></code></pre></div><p>That is the pattern. Capture. Store. Restore.</p>
<p>The code only becomes difficult when the object graph is bigger than the example, which of course is where game code lives most of the time.</p>
<h2 id="why-not-just-expose-the-fields">Why not just expose the fields?</h2>
<p>The lazy answer is tempting: make a few properties public, let the save manager read them, and move on.</p>
<p>That can work for a throwaway prototype. It becomes expensive when those fields stop being a clean representation of the gameplay state. A health component might cache derived values. An inventory might index items by slot and by item id. A quest system might store runtime-only listeners next to persistent quest progress. A board game model might keep temporary highlights, animation hints, and deterministic state in the same object.</p>
<p>If the save manager knows all of that, the ownership is backwards. The save manager becomes the real expert on every subsystem, and every gameplay refactor now risks breaking save, undo, replay, and tests at the same time.</p>
<p>A memento keeps the decision near the code that owns the invariants. The inventory decides what an inventory snapshot means. The quest log decides which fields are durable progress and which fields are presentation noise. The puzzle board decides whether current animations matter when restoring a move.</p>
<p>That does not remove the work. It puts the work in the place least likely to lie.</p>
<h2 id="memento-is-not-the-same-as-serialize-everything">Memento is not the same as “serialize everything”</h2>
<p>Unity makes this distinction easy to blur because serialization is everywhere. Scenes serialize components. Prefabs serialize fields. ScriptableObjects serialize content. JSON tools serialize DTOs. It is natural to reach for the same word when building save data.</p>
<p>A memento is a snapshot of meaningful state. Serialization is one possible way to store or transport that snapshot.</p>
<p>Those are different decisions.</p>
<p>For an undo stack inside an editor tool, the memento may never leave memory. A compact immutable object is enough. For a checkpoint inside a single level, it might be acceptable to hold a snapshot until the player leaves the scene. For a real save slot, the snapshot needs a stable format, versioning, validation, and failure handling. For netcode rollback, the snapshot probably needs to be small, deterministic, and cheap to copy.</p>
<p>Do not let the storage format choose the game architecture. Start with the restore contract: what state must come back, at what point in the frame, under which version of the code, and with which external dependencies available?</p>
<p>Only after that should you decide whether the memento becomes JSON, binary, a ScriptableObject clone, a custom byte buffer, or a plain in-memory record.</p>
<h2 id="the-first-trap-shallow-snapshots">The first trap: shallow snapshots</h2>
<p>The most common memento bug is accidentally keeping references to mutable objects.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> InventorySnapshot CreateSnapshot()
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> InventorySnapshot(items); <span style="color:#75715e">// Bug: same list, same item objects.</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This looks like a snapshot, but it is just a second handle to live state. If the inventory changes, the “snapshot” changes with it. Restore might appear to work in simple tests because the list still has familiar values, then fail when an item stack mutates in place.</p>
<p>For mutable state, a snapshot needs its own copy of the data it promises to preserve.</p>
<p>That does not always mean a deep copy of the entire world. It means copying the state at the boundary you are restoring. If an inventory snapshot stores item ids and counts, copy those values. If an item instance has durability, affixes, and generated identity, copy those values too. If item definitions live in immutable data assets, store an id or reference to the definition instead of cloning the definition itself.</p>
<p>The useful question is: after capture, can normal gameplay mutate something that the snapshot depends on?</p>
<p>If yes, the snapshot is not isolated enough.</p>
<h2 id="the-second-trap-saving-runtime-objects-instead-of-state">The second trap: saving runtime objects instead of state</h2>
<p>A memento should rarely store live engine objects as the main truth.</p>
<p>In Unity, a <code>GameObject</code>, <code>Transform</code>, <code>MonoBehaviour</code>, asset reference, or event subscription is usually not the state you want to save. It is runtime machinery around the state. Sometimes a reference is fine as a lookup key inside one scene. For long-lived saves and robust undo, prefer data that can be validated and reconstructed.</p>
<p>For example, this is fragile save data:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">DoorSnapshot</span>(GameObject DoorObject, <span style="color:#66d9ef">bool</span> IsOpen);
</span></span></code></pre></div><p>This is usually better:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">DoorSnapshot</span>(<span style="color:#66d9ef">string</span> DoorId, <span style="color:#66d9ef">bool</span> IsOpen, <span style="color:#66d9ef">float</span> OpenAmount);
</span></span></code></pre></div><p>The second version gives the restore code room to say useful things. If <code>DoorId</code> is missing, the save can report a broken reference. If the door prefab changed, the restore path can map old ids to new ids. If the game loads additively, the save system can wait until the relevant scene is present before applying the snapshot.</p>
<p>Live references are convenient until the object is destroyed, renamed, pooled, unloaded, replaced by a prefab variant, or moved to another scene. State survives those changes better than objects do.</p>
<h2 id="undo-stacks-want-different-mementos-than-save-files">Undo stacks want different mementos than save files</h2>
<p>Undo and save are often taught together, but they have different pressures.</p>
<p>An undo stack usually lives for minutes. It can store richer data because it only needs to survive the current session. It may also care about editor-facing details that a save file should ignore: current selection, temporary tool mode, or a user’s unsaved graph layout.</p>
<p>A save file may need to survive months, patches, platform differences, partial corruption, and player support tickets. It should be boring, versioned, and independent from incidental runtime details. It should use stable identifiers. It should validate input before mutating the live game.</p>
<p>Because of that, I usually avoid one universal snapshot type for every purpose. The board editor can have a <code>BoardUndoSnapshot</code>. The save system can have a <code>BoardSaveData</code>. They may share conversion helpers internally, but they do not need to pretend the same shape serves both jobs.</p>
<p>This is one of those places where duplication can be cheaper than false reuse. Two small DTOs with clear jobs beat one “global snapshot” type full of fields that only some callers understand.</p>
<h2 id="keep-restoration-boring-and-atomic">Keep restoration boring and atomic</h2>
<p>Restoring state is where damage happens.</p>
<p>Capture bugs are annoying. Restore bugs can corrupt the live model. A good restore path should be boring, explicit, and hard to half-apply.</p>
<p>For small objects, clearing and rebuilding can be safer than trying to patch the existing state in place:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Restore(BoardSnapshot snapshot)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    cells.Clear();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">foreach</span> (<span style="color:#66d9ef">var</span> cell <span style="color:#66d9ef">in</span> snapshot.Cells)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        cells[cell.Position] = <span style="color:#66d9ef">new</span> BoardCell(cell.TileId, cell.ActorId);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    RebuildLookupTables();
</span></span><span style="display:flex;"><span>    RaiseBoardChanged();
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The important part is not the exact code. The important part is the sequence:</p>
<ol>
<li>discard or isolate the old mutable state</li>
<li>apply the snapshot data</li>
<li>rebuild derived caches</li>
<li>notify the rest of the game once</li>
</ol>
<p>That last step matters. If every restored cell fires an event, subscribers may see a half-restored board. UI can flicker. AI can react to invalid intermediate state. Network code can replicate nonsense.</p>
<p>When restore is a batch operation, treat it as a batch operation. Suppress per-field notifications if needed, rebuild the model, then publish one clear change event.</p>
<h2 id="validate-before-you-mutate">Validate before you mutate</h2>
<p>Save files and remote snapshots are trust boundaries. Undo snapshots made inside the same process are usually safer, but even those can become stale after a tool changes.</p>
<p>Before applying a memento, check the cheap invariants:</p>
<ul>
<li>is the snapshot version supported?</li>
<li>are required ids present?</li>
<li>are counts inside allowed limits?</li>
<li>do referenced definitions exist?</li>
<li>are enum values known?</li>
<li>are positions inside the board or level bounds?</li>
<li>can this snapshot be applied in the current game mode?</li>
</ul>
<p>Do those checks before touching live state. If validation fails halfway through restore, the player should not end up with a partly loaded inventory and a broken quest log.</p>
<p>For larger saves, I like a two-step flow: parse into raw save data, validate and resolve ids into a restore plan, then apply the plan. The restore plan is allowed to contain resolved references because it is short-lived and already checked. The durable memento stays simple and portable.</p>
<p>That sounds heavier than the toy pattern, but it is still the same idea. The caretaker holds data. The originator controls how that data becomes valid state again.</p>
<h2 id="versioning-is-part-of-the-pattern-in-shipped-games">Versioning is part of the pattern in shipped games</h2>
<p>A memento example in a design-pattern article does not usually talk about versioning. A shipped game has to.</p>
<p>The moment a snapshot leaves memory and lands on disk, it becomes a contract with future builds. Fields will be renamed. Items will be removed. Quest logic will change. Balance data will move from one table to another. If the save format has no version, the restore code has to guess which world it is reading.</p>
<p>Put a version on durable snapshots early.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">SaveGameV3</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> Version,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">string</span> PlayerId,
</span></span><span style="display:flex;"><span>    InventorySaveData Inventory,
</span></span><span style="display:flex;"><span>    QuestSaveData Quests,
</span></span><span style="display:flex;"><span>    WorldSaveData World);
</span></span></code></pre></div><p>The version does not need to be fancy. It just needs to let the loader choose a migration path. A common setup is:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>SaveGameV3 save = raw.Version <span style="color:#66d9ef">switch</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">1</span> =&gt; MigrateV1ToV3(raw),
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">2</span> =&gt; MigrateV2ToV3(raw),
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">3</span> =&gt; ParseV3(raw),
</span></span><span style="display:flex;"><span>    _ =&gt; <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> UnsupportedSaveVersionException(raw.Version)
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>For some games, old saves can be abandoned. For others, compatibility is a serious player trust issue. Either way, make the decision explicit. Silent guessing is how small save changes become unreproducible bug reports.</p>
<h2 id="where-mementos-fit-with-commands">Where mementos fit with commands</h2>
<p>The command pattern and memento pattern pair naturally, especially for undo.</p>
<p>A command says what to do. A memento remembers what the world looked like before the command did it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MoveUnitCommand</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> BoardSnapshot? before;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Execute(Board board, UnitId unit, CellPosition target)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        before = board.CreateSnapshot();
</span></span><span style="display:flex;"><span>        board.MoveUnit(unit, target);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Undo(Board board)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (before <span style="color:#66d9ef">is</span> <span style="color:#66d9ef">null</span>) <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>        board.Restore(before);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is deliberately simple. In a serious tactics game, snapshotting the entire board for every move may be too expensive. The upgrade path is to store a smaller delta: unit id, old position, new position, previous action points, captured unit state, and any changed tile effects.</p>
<p>But start with the whole-board snapshot if it is cheap enough. It is easier to test, easier to reason about, and less likely to miss a side effect. Optimize when measurements show that the snapshot cost matters.</p>
<p>That is the practical version of laziness: begin with the lowest-risk correct model, not the cleverest one.</p>
<h2 id="whole-snapshots-versus-deltas">Whole snapshots versus deltas</h2>
<p>A memento can store a full snapshot or a delta.</p>
<p>A <strong>full snapshot</strong> captures everything needed to restore the target object. It is straightforward and robust. It can be expensive if the state is large or captured often.</p>
<p>A <strong>delta snapshot</strong> captures only what changed. It is smaller and often faster, but it requires more careful restore logic. If the command changes five systems and the delta records four of them, undo becomes a source of ghosts.</p>
<p>For game code, my default split is:</p>
<ul>
<li>use full snapshots for small models, prototypes, tools, and low-frequency checkpoints</li>
<li>use deltas for high-frequency undo, rollback, or large worlds</li>
<li>use explicit save DTOs for durable player saves</li>
<li>avoid raw object graphs unless the lifetime is tightly controlled</li>
</ul>
<p>Measure before making this complicated. A 20 KB board snapshot taken once per player move is probably fine. A 20 KB snapshot taken sixty times per second for rollback is not.</p>
<h2 id="a-workable-unity-boundary">A workable Unity boundary</h2>
<p>In Unity projects, I prefer to keep snapshot logic out of random <code>MonoBehaviour</code> components where possible.</p>
<p>A <code>MonoBehaviour</code> is a good adapter to the scene. It is not always a good owner of durable state. The cleaner shape is often a plain C# model with memento methods, plus a Unity-facing component that visualizes or synchronizes that model.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PuzzleBoardModel</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> Dictionary&lt;Vector2Int, TileState&gt; tiles = <span style="color:#66d9ef">new</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> PuzzleBoardSnapshot CreateSnapshot() =&gt;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span>(tiles.Select(pair =&gt; TileSnapshot.From(pair.Key, pair.Value)).ToArray());
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Restore(PuzzleBoardSnapshot snapshot)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        tiles.Clear();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">foreach</span> (<span style="color:#66d9ef">var</span> tile <span style="color:#66d9ef">in</span> snapshot.Tiles)
</span></span><span style="display:flex;"><span>            tiles[tile.Position] = tile.ToState();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PuzzleBoardView</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> TileView tilePrefab;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Render(PuzzleBoardModel model)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Update pooled views from model state.</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That boundary pays off in tests. The model can be tested without a scene. Snapshots can be compared as plain data. Restore can run in edit-mode tests or a console runner. The Unity layer still matters, but it no longer owns every decision about persistence.</p>
<p>Some projects cannot draw that line cleanly, especially late in production. Fine. Put the memento methods where the state actually lives. Just do not make the save manager crawl through every component with reflection and hope that the result is a valid game.</p>
<h2 id="a-small-checklist-before-using-memento">A small checklist before using memento</h2>
<p>The pattern is worth reaching for when at least one of these is true:</p>
<ul>
<li>callers need undo, redo, checkpoints, rollback, or speculative changes</li>
<li>the object has private state that should not be exposed just for saving</li>
<li>restore must rebuild invariants or derived caches</li>
<li>snapshots need to be stored by a manager that should not understand internals</li>
<li>the save format should stay stable while implementation details change</li>
</ul>
<p>It is probably overkill when:</p>
<ul>
<li>the object is already a tiny immutable value</li>
<li>a direct copy constructor is enough</li>
<li>the state is never restored</li>
<li>exposing data is already the point of the type</li>
<li>the snapshot would be larger and more complex than the object itself</li>
</ul>
<p>Patterns are tools, not achievements. The memento pattern earns its keep when it protects ownership. If it only adds ceremony around simple data, skip it.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Use mementos to draw a clean line between “someone needs to hold this previous state” and “someone gets to understand this object’s internals.”</p>
<p>For in-memory undo, start with full snapshots unless they are clearly too expensive. For durable saves, make snapshots explicit data contracts with stable ids, validation, and versioning. For Unity projects, keep the durable state as plain data when you can, and let scene objects rebuild from it instead of becoming the save format themselves.</p>
<p>The point is not to worship the pattern name. The point is to make restore boring, local, and safe. Future-you will appreciate that more than a clever save manager that knows too much.</p>
<h2 id="references">References</h2>
<ul>
<li>Refactoring.Guru, “Memento” — overview of the originator, memento, and caretaker roles.</li>
<li>Unity Manual, “Script serialization” — useful context for what Unity serializes and why engine serialization is not the same as a save-game contract.</li>
<li>Microsoft Learn, “System.Text.Json overview” — practical reference for JSON serialization when a snapshot needs to become durable data.</li>
<li>Robert Nystrom, <em>Game Programming Patterns</em>, “Command” — helpful companion pattern for undo and action history.</li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>R3 and Reactive Programming in Unity: Use Streams Where Events Start to Fray</title>
            <link>https://blog.jonmikeys.com/posts/r3-reactive-programming-in-unity/</link>
            <pubDate>Fri, 28 Aug 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/r3-reactive-programming-in-unity/</guid>
            <description>A practical look at using R3-style reactive streams in Unity without turning every button click, timer, and gameplay signal into architecture theater.</description>
            <content type="html"><![CDATA[<p>Reactive programming is useful in Unity when a system is mostly about values changing over time.</p>
<p>That sounds like most game code, so the first trap is obvious: once the tool clicks, it becomes tempting to turn everything into a stream. Health changes, button clicks, cooldowns, input, animation events, network state, scene loading, save status, enemy awareness, every single thing. The code looks clever for a week, then someone has to debug a chain of operators at 2 a.m. because the inventory UI updated twice and nobody knows who subscribed first.</p>
<p>R3 is worth looking at because it modernizes the old UniRx shape and takes Unity seriously. It gives C# developers a way to compose event-like flows, frame-based timing, and lifetime-bound subscriptions without pretending Unity is a normal server application. But the useful version of reactive programming is not &ldquo;make all code reactive.&rdquo; The useful version is narrower: use streams where a value or event has multiple consumers, changes over time, and benefits from composition.</p>
<p><img src="/images/posts/r3-reactive-programming-in-unity/diagram.svg" alt="R3 Reactive Programming In Unity diagram"></p>
<h2 id="the-problem-reactive-code-actually-solves">The problem reactive code actually solves</h2>
<p>A lot of Unity code starts with a simple callback.</p>
<p>A button calls a method. A health component raises an event. A loading system invokes a completion handler. That is fine. Direct code is still the cheapest architecture when the relationship is obvious and local.</p>
<p>The trouble starts when the same signal needs filtering, combining, throttling, lifetime management, or multiple consumers. A health value is not just used by the health bar. It might drive damage effects, low-health audio, AI targeting, achievements, analytics, save data, tutorial prompts, and controller rumble. If every one of those systems wires itself directly to the health component, the feature still works, but the dependencies become noisy.</p>
<p>Reactive programming gives that problem a different shape. Instead of treating each notification as a one-off callback, it treats the changing value as a stream. Code can subscribe to that stream, transform it, filter it, combine it with other streams, and dispose the subscription when the listener goes away.</p>
<p>The idea is not complicated. The debugging can be, so the boundaries matter.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>raw event/value
</span></span><span style="display:flex;"><span>     │
</span></span><span style="display:flex;"><span>     ▼
</span></span><span style="display:flex;"><span> observable stream
</span></span><span style="display:flex;"><span>     │
</span></span><span style="display:flex;"><span>     ├── filter: only when meaningful
</span></span><span style="display:flex;"><span>     ├── map: convert to what this system needs
</span></span><span style="display:flex;"><span>     ├── combine: include another signal
</span></span><span style="display:flex;"><span>     ▼
</span></span><span style="display:flex;"><span> subscribers tied to object lifetime
</span></span></code></pre></div><p>That pipeline is the reason to use R3. If the pipeline is not buying clarity, do not pay for it.</p>
<h2 id="r3-in-one-practical-sentence">R3 in one practical sentence</h2>
<p>R3 is a modern Reactive Extensions implementation from Cysharp, the same ecosystem behind UniTask, with support for Unity and other .NET platforms.</p>
<p>The important part for game code is not the brand name. It is the model: an <code>Observable&lt;T&gt;</code> represents values over time, and a subscription observes those values until it is disposed. This is close to the observer pattern, but with a richer vocabulary for composing streams.</p>
<p>In plain C#, an event says, &ldquo;call these handlers when something happens.&rdquo; A reactive stream says, &ldquo;this is a sequence of things happening over time, and you can build a pipeline over it.&rdquo;</p>
<p>That difference matters when code stops being a single notification.</p>
<p>For example, &ldquo;the player clicked this button&rdquo; can be an event. &ldquo;The player clicked this button, but only while the panel is visible, not more than once per second, and then we need to request data and cancel the old request if the panel closes&rdquo; is no longer just an event. It is a flow. Flows are where reactive code starts to earn its keep.</p>
<h2 id="a-small-health-example">A small health example</h2>
<p>Start with something boring: a health component that exposes the current value.</p>
<p>The exact R3 API surface can change between versions, so treat this as the shape to aim for rather than a paste-and-forget package tutorial. The important pieces are a reactive value, read-only exposure, subscription, and disposal.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> R3;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Health</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> maxHealth = <span style="color:#ae81ff">100</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ReactiveProperty&lt;<span style="color:#66d9ef">int</span>&gt; current = <span style="color:#66d9ef">new</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> ReadOnlyReactiveProperty&lt;<span style="color:#66d9ef">int</span>&gt; Current =&gt; current;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> MaxHealth =&gt; maxHealth;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> Awake()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        current.Value = maxHealth;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> TakeDamage(<span style="color:#66d9ef">int</span> amount)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (amount &lt;= <span style="color:#ae81ff">0</span>) <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>        current.Value = Mathf.Max(<span style="color:#ae81ff">0</span>, current.Value - amount);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now a UI view can listen without the health component knowing anything about sliders, text, color flashes, or animation.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> R3;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine.UI;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">HealthBarView</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> Health health;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> Slider slider;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> CompositeDisposable disposables = <span style="color:#66d9ef">new</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnEnable()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        health.Current
</span></span><span style="display:flex;"><span>            .Select(<span style="color:#66d9ef">value</span> =&gt; (<span style="color:#66d9ef">float</span>)<span style="color:#66d9ef">value</span> / health.MaxHealth)
</span></span><span style="display:flex;"><span>            .Subscribe(normalized =&gt; slider.<span style="color:#66d9ef">value</span> = normalized)
</span></span><span style="display:flex;"><span>            .AddTo(disposables);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnDisable()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        disposables.Clear();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>There is no grand architecture here. The UI is just reacting to a value. That is the level where this style is easiest to justify.</p>
<p>The health component owns health. The view owns presentation. The subscription is tied to the view lifetime. If the UI is disabled, it stops listening. No hidden manager is required.</p>
<h2 id="the-lifetime-rule-is-not-optional">The lifetime rule is not optional</h2>
<p>Most reactive bugs in Unity are not caused by <code>Select</code>, <code>Where</code>, or other operators. They are caused by subscriptions that outlive the object that made them.</p>
<p>Unity destroys objects differently from normal C# object lifetimes. A C# reference can remain while the underlying Unity object is gone. If a subscription keeps calling into that destroyed object, the failure may show up as a null-looking Unity object, a missing reference exception, duplicate UI updates after scene reload, or a listener that appears to survive forever.</p>
<p>So the boring rule is the important rule: every subscription needs an owner.</p>
<p>For a <code>MonoBehaviour</code>, the owner is usually the component lifetime. For a panel, it may be <code>OnEnable</code> to <code>OnDisable</code>. For a gameplay entity, it may be spawn to despawn. For a service that lives through the whole application, it can be application lifetime, but that should be an explicit decision rather than an accident.</p>
<p>This is where a <code>CompositeDisposable</code> is useful. It is not glamorous. It is just a list of cleanup handles. Add subscriptions to it, clear or dispose it at the matching lifetime boundary, and most leak-shaped problems disappear.</p>
<p>The mistake is subscribing in <code>Start</code> and forgetting about it because the scene looked fine during a single play session. Games are full of repetition: menus reopen, scenes reload, pooled enemies respawn, HUD elements are replaced, players reconnect. A subscription leak might be invisible on the first loop and obvious on the fifth.</p>
<p>That is not a reactive-specific lesson, really. It is the same ownership problem we already have with events. Reactive code just makes it easier to create more subscriptions, so it makes the discipline more important.</p>
<h2 id="where-streams-beat-plain-events">Where streams beat plain events</h2>
<p>A plain C# event is excellent when the sender and receiver already have a clean relationship.</p>
<p>A door opens when a pressure plate is pressed. A weapon notifies its reticle that ammo changed. A local component tells a sibling component that setup finished. Do not turn that into a stream because a blog post said streams are elegant. That is how clean code becomes ceremonial code.</p>
<p>Streams are better when there is actual composition.</p>
<p>Input is a good example. A raw button press is rarely the final behavior. The game may need to ignore input while a menu is open, detect a hold, debounce repeated clicks, combine aim direction with fire input, or switch bindings depending on player state. Each rule is small. The combined flow can become messy when scattered across <code>Update</code> flags.</p>
<p>UI is another good fit. UI code tends to be event-heavy, state-heavy, and lifetime-heavy. Buttons, toggles, sliders, text fields, selected items, loading spinners, and network responses all change over time. A stream-based view model can make the data flow easier to see, as long as the team resists the urge to hide every assignment behind an operator chain.</p>
<p>Gameplay state can also fit, especially when a derived value depends on several inputs. A stamina warning might depend on stamina, sprint state, combat state, and a settings flag. If those inputs are already streams, deriving the warning as a stream can be cleaner than manually remembering which event should recalculate which property.</p>
<p>The pattern is simple: use reactive code where the sentence contains &ldquo;when this changes, while that is true, convert it to this, and stop when this owner dies.&rdquo; If the sentence is &ldquo;call this method,&rdquo; call the method.</p>
<h2 id="where-streams-make-code-worse">Where streams make code worse</h2>
<p>Reactive programming is not free.</p>
<p>The cost is not only runtime overhead, though that can matter in hot paths. The bigger cost is readability. A direct method call gives you a stack trace and a line of code. A reactive pipeline gives you a chain. Chains are readable when short and named well. They become miserable when every feature is a nested stream transformation with side effects at the end.</p>
<p>Avoid reactive code for core per-frame simulation unless there is a strong reason. Movement integration, collision response, animation sampling, and tight combat loops usually want direct, predictable code. If something runs for thousands of entities every frame, do not add a stream abstraction just to feel modern. Use data-oriented code, jobs, Burst, plain arrays, or boring loops where they fit.</p>
<p>Also avoid streams as a substitute for ownership. If nobody knows which system owns inventory state, exposing inventory as observables does not fix the design. It only gives the confusion a nicer API.</p>
<p>A useful test is whether deleting the reactive layer would make the code clearer. If the answer is yes, delete it. That is not anti-pattern purism. That is just maintenance.</p>
<h2 id="reactive-properties-are-state-not-events">Reactive properties are state, not events</h2>
<p>One common mistake is treating all streams as the same thing.</p>
<p>A health value and a death notification are related, but they are not identical. Health is state. Death is an event derived from a transition in that state. If those are mixed carelessly, subscribers start doing awkward work to guess meaning from a value.</p>
<p>For example, a UI health bar wants the current health even if it subscribes after the player has already taken damage. A death animation probably does not want to replay just because a listener subscribes late and sees that health is already zero.</p>
<p>That distinction should shape the API.</p>
<p>Expose durable values as reactive properties or state streams. Expose one-time moments as event streams. If an event is derived from state, make the transition explicit so late subscribers do not accidentally replay old moments.</p>
<p>In practical terms, ask one question before exposing a stream: should a new subscriber receive the latest value immediately?</p>
<p>If yes, it is probably state.
If no, it is probably an event.</p>
<p>That one question prevents a surprising amount of weird UI and gameplay behavior.</p>
<h2 id="a-cleaner-ui-flow">A cleaner UI flow</h2>
<p>Here is a slightly more realistic UI example. Imagine a character select panel with a selected character, a ready button, and a player profile that may or may not be loaded.</p>
<p>Without a stream model, this often becomes a handful of methods that all call <code>RefreshReadyButton()</code>: <code>OnCharacterSelected</code>, <code>OnProfileLoaded</code>, <code>OnProfileFailed</code>, <code>OnPanelOpened</code>, <code>OnPanelClosed</code>, and maybe <code>Update</code> if something was missed.</p>
<p>Reactive code lets the view say what the ready state depends on.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>selectedCharacter
</span></span><span style="display:flex;"><span>    .CombineLatest(profileState, (character, profile) =&gt;
</span></span><span style="display:flex;"><span>        character != <span style="color:#66d9ef">null</span> &amp;&amp; profile.IsLoaded &amp;&amp; profile.CanUse(character))
</span></span><span style="display:flex;"><span>    .DistinctUntilChanged()
</span></span><span style="display:flex;"><span>    .Subscribe(canReady =&gt; readyButton.interactable = canReady)
</span></span><span style="display:flex;"><span>    .AddTo(disposables);
</span></span></code></pre></div><p>That is the good version: short, declarative, and tied to the UI lifetime.</p>
<p>The bad version is the same idea stretched until nobody wants to touch it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>selectedCharacter
</span></span><span style="display:flex;"><span>    .Where(character =&gt; character != <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>    .SelectMany(character =&gt; LoadCosmetics(character))
</span></span><span style="display:flex;"><span>    .CombineLatest(profileState, inventoryState, matchmakingState,
</span></span><span style="display:flex;"><span>        (cosmetics, profile, inventory, matchmaking) =&gt; <span style="color:#75715e">/* too much */</span>)
</span></span><span style="display:flex;"><span>    .Where(result =&gt; <span style="color:#75715e">/* mystery rule */</span>)
</span></span><span style="display:flex;"><span>    .Subscribe(result =&gt; <span style="color:#75715e">/* changes five systems */</span>);
</span></span></code></pre></div><p>At that point, split the work. Give important decisions names. Put domain rules in normal methods. Keep the stream as plumbing, not as the only place where the game rules exist.</p>
<p>Reactive code is easiest to maintain when operators describe flow and named functions describe business rules.</p>
<h2 id="r3-unirx-and-asyncawait-are-different-tools">R3, UniRx, and async/await are different tools</h2>
<p>R3 sits in the same family tree as UniRx, but it is not just a nostalgia package. Cysharp positions it as a newer Reactive Extensions implementation that learned from UniRx, modern .NET, and game-engine constraints. UniRx still matters historically, and plenty of Unity projects have shipped with it, but new projects should at least evaluate R3 before defaulting to older patterns.</p>
<p>It is also worth separating reactive streams from async/await.</p>
<p>Async/await is great for a single operation that starts, waits, and completes. Load this asset. Fetch this profile. Save this file. Wait for this animation step. UniTask fits that world well in Unity because it keeps async code aligned with the PlayerLoop and avoids some of the allocation problems of plain <code>Task</code> in gameplay-adjacent code.</p>
<p>Reactive streams are better for ongoing sequences. Button clicks. Value changes. Input state. Network status updates. Cooldown ticks. Selection changes. The stream may not have one natural completion point, because the feature exists as long as the owner exists.</p>
<p>A loading screen might use both. The button click is a stream. The load operation kicked off by the click is async. The progress value may be a stream. The final scene activation may be awaited. Trying to force all of that into one abstraction usually makes the code worse.</p>
<p>Use the tool that matches the shape of the work.</p>
<h2 id="performance-assume-nothing-measure-the-hot-path">Performance: assume nothing, measure the hot path</h2>
<p>Reactive code has overhead. So do events, virtual calls, interfaces, allocations, coroutines, LINQ, logging, and bad <code>Update</code> loops. The question is not whether overhead exists. The question is whether it matters in the place you are using it.</p>
<p>For UI, menus, save status, tool workflows, and occasional gameplay notifications, the readability can easily be worth it. For thousands of enemies evaluating streams every frame, it probably is not. For a combat HUD updating a few values, it might be fine. For projectile simulation, stop being cute and use a loop.</p>
<p>R3 specifically calls out performance concerns in older Rx designs and includes game-engine-oriented ideas such as frame-based operations. That is encouraging, but it does not excuse guessing. If the code is near a frame budget, benchmark it in the target environment. Editor numbers are useful for smoke tests, not final truth. IL2CPP, device CPU, allocations, and scene scale all matter.</p>
<p>The lazy senior answer is boring: keep reactive code away from the hottest loops by default, then measure if you want to move the boundary.</p>
<h2 id="debugging-reactive-code-without-suffering">Debugging reactive code without suffering</h2>
<p>The easiest way to debug reactive code is to not make it mysterious in the first place.</p>
<p>Keep pipelines short. Give derived streams names. Avoid side effects in the middle of a chain. Put logging at meaningful boundaries. If a subscription changes scene state, make that subscription easy to find. If a stream represents an important concept, expose it from a class with a name that explains ownership.</p>
<p>Bad reactive code tends to hide verbs. Everything is <code>Select</code>, <code>Where</code>, <code>Merge</code>, and <code>Subscribe</code>, but the gameplay action is buried in lambdas. Good reactive code names the thing the game cares about: <code>CanStartMatch</code>, <code>SelectedLoadout</code>, <code>LowHealthWarning</code>, <code>ConnectionLost</code>, <code>ReadyButtonPressed</code>.</p>
<p>That naming is not decoration. It is how another programmer debugs the feature without rebuilding the whole mental pipeline.</p>
<p>Also watch for accidental multiple subscriptions. A cold-looking stream that does work per subscriber can surprise you if three systems subscribe and the work runs three times. This is not unique to R3, but reactive APIs make it easy to forget that subscription is an action, not just a reference.</p>
<p>If subscribing causes work, treat it with the same suspicion you would give any method call with side effects.</p>
<h2 id="a-practical-adoption-path">A practical adoption path</h2>
<p>Do not begin by rewriting architecture.</p>
<p>Start with one contained feature where events are already messy. A menu panel is ideal. A HUD view is fine. A debug tool is fine. A settings screen with sliders and toggles is fine. Pick something with clear lifetime boundaries and low performance risk.</p>
<p>Use reactive properties for view-facing state. Use event streams for user actions. Dispose subscriptions at <code>OnDisable</code> or <code>OnDestroy</code>, depending on how the object is reused. Keep domain logic in named methods. Keep the stream chains boring.</p>
<p>Then review the result honestly.</p>
<p>Did it remove state synchronization bugs? Did it reduce duplicated <code>Refresh()</code> calls? Can a teammate read it without knowing every operator by heart? Does the lifetime cleanup survive reopening the panel ten times? If yes, expand slowly. If no, delete it and go back to events. No architecture decision should be too prestigious to remove.</p>
<p>The safest adoption pattern is to use R3 at the edges first: UI, tools, input glue, loading status, and presentation state. Let core simulation stay direct until the need is obvious.</p>
<h2 id="takeaway">Takeaway</h2>
<p>R3 is a good fit for Unity code that is already event-heavy, lifetime-sensitive, and full of values changing over time. It gives those flows a composable vocabulary, especially in UI and presentation layers where direct callbacks tend to multiply.</p>
<p>But streams are not a personality. They are plumbing.</p>
<p>Use them when they make the data flow clearer. Tie every subscription to a lifetime. Keep hot simulation code boring. Separate state streams from event streams. Name important derived values instead of burying game rules inside operator chains.</p>
<p>The practical win is not that the project becomes &ldquo;reactive.&rdquo; The win is that the next programmer can see what changes, who reacts, and when the reaction stops.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://github.com/Cysharp/R3">Cysharp R3 README</a> — project goals, Unity support, and differences from older Rx approaches.</li>
<li><a href="https://github.com/neuecc/UniRx">UniRx README</a> — historical Unity Reactive Extensions context and the path that led many Unity teams toward reactive workflows.</li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/standard/events/observer-design-pattern">Microsoft Learn: Observer design pattern</a> — background on providers, observers, and subscriptions in .NET.</li>
<li><a href="https://learn.unity.com/tutorial/create-modular-and-maintainable-code-with-the-observer-pattern">Unity Learn: Create modular and maintainable code with the observer pattern</a> — Unity-oriented discussion of loose coupling through observer-style design.</li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Builder and Fluent Builder Patterns: Make Setup Readable Without Hiding the Cost</title>
            <link>https://blog.jonmikeys.com/posts/builder-fluent-builder-patterns/</link>
            <pubDate>Mon, 24 Aug 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/builder-fluent-builder-patterns/</guid>
            <description>A practical guide to builder-style APIs in game code: when they clarify setup, when object initializers are enough, and where fluent chains become a trap.</description>
            <content type="html"><![CDATA[<p>Builder APIs are useful when object setup has more rules than a constructor can explain.</p>
<p>That is the short version. The longer version is where people usually get into trouble. A builder can make complicated setup readable, guide callers through valid options, and keep construction code out of gameplay logic. It can also turn three obvious assignments into a maze of chained methods that nobody wants to debug at 2 a.m.</p>
<p>Game code hits both sides often. We build weapons from data, spawn enemies with tuning overrides, configure UI widgets, assemble VFX requests, create test fixtures, and send messages across network or service boundaries. Some of those objects deserve a builder. Most do not.</p>
<p>Writing a builder is easy. Deciding whether the extra type earns its keep is the real work.</p>
<p><img src="/images/posts/builder-fluent-builder-patterns/diagram.svg" alt="Builder flow"></p>
<h2 id="what-a-builder-is-actually-buying-you">What a builder is actually buying you</h2>
<p>A builder separates <strong>configuration</strong> from <strong>creation</strong>.</p>
<p>Instead of forcing every option through one constructor, the caller fills in a small construction object. When the configuration is complete, the builder creates the final object in one place. A fluent builder is the same idea with methods that return the builder so calls can be chained.</p>
<p>The shape usually looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> projectile = <span style="color:#66d9ef">new</span> ProjectileBuilder(prefab)
</span></span><span style="display:flex;"><span>    .WithOwner(player)
</span></span><span style="display:flex;"><span>    .WithDamage(<span style="color:#ae81ff">25</span>)
</span></span><span style="display:flex;"><span>    .WithSpeed(<span style="color:#ae81ff">40f</span>)
</span></span><span style="display:flex;"><span>    .WithLifetime(<span style="color:#ae81ff">3f</span>)
</span></span><span style="display:flex;"><span>    .Build();
</span></span></code></pre></div><p>This is not automatically better than a constructor or object initializer. It is better only if the construction step needs help.</p>
<p>A builder can help when:</p>
<ul>
<li>some options are required and others are optional</li>
<li>the valid combinations are not obvious</li>
<li>defaults should be centralized</li>
<li>construction needs validation before the object exists</li>
<li>the final object should be immutable after creation</li>
<li>call sites are currently full of noisy setup code</li>
<li>tests need clear, reusable fixture setup</li>
</ul>
<p>Those are real problems. A builder gives them a home.</p>
<p>But if the class has two required values and no invariants, a builder is decoration. Decoration is more code to read.</p>
<h2 id="start-with-the-boring-options-first">Start with the boring options first</h2>
<p>Before writing a builder, check whether C# already gives you the shape you need. It often does.</p>
<p>For simple data carriers, an object initializer is hard to beat:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> spawn = <span style="color:#66d9ef">new</span> EnemySpawnRequest
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    EnemyId = <span style="color:#e6db74">&#34;grunt&#34;</span>,
</span></span><span style="display:flex;"><span>    Position = spawnPoint.position,
</span></span><span style="display:flex;"><span>    Level = <span style="color:#ae81ff">3</span>,
</span></span><span style="display:flex;"><span>    AggroRadius = <span style="color:#ae81ff">12f</span>
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>That is readable, searchable, and unsurprising. If the type uses C# <code>required</code> members, the compiler can force the important fields to be set:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">EnemySpawnRequest</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> required <span style="color:#66d9ef">string</span> EnemyId { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">init</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> required Vector3 Position { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">init</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> Level { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">init</span>; } = <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> AggroRadius { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">init</span>; } = <span style="color:#ae81ff">8f</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>For many request objects, that is enough. No builder. No chain. No private mutable state hiding in another type.</p>
<p>Records and <code>init</code> properties can cover the same ground when you want values that are easy to copy with small changes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> eliteSpawn = baseSpawn with
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    EnemyId = <span style="color:#e6db74">&#34;elite_grunt&#34;</span>,
</span></span><span style="display:flex;"><span>    Level = <span style="color:#ae81ff">8</span>
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>That is useful for tools, tests, and data transformations. It is not always the right fit for Unity runtime objects, especially if serialization or engine object lifetimes are involved, but the point stands: use the language feature when the language feature solves the problem.</p>
<p>A builder should appear after these simpler tools stop carrying the weight.</p>
<h2 id="the-constructor-problem-builders-actually-solve">The constructor problem builders actually solve</h2>
<p>Constructors become painful when optional values pile up.</p>
<p>This is the warning sign:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> weapon = <span style="color:#66d9ef">new</span> WeaponDefinition(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;burst_rifle&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">30</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0.08f</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">1.6f</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">2.5f</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0.35f</span>);
</span></span></code></pre></div><p>Nobody wants to review that call. The compiler knows the types, but the reader has to count arguments and remember what each number means. Named arguments can help:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> weapon = <span style="color:#66d9ef">new</span> WeaponDefinition(
</span></span><span style="display:flex;"><span>    id: <span style="color:#e6db74">&#34;burst_rifle&#34;</span>,
</span></span><span style="display:flex;"><span>    damage: <span style="color:#ae81ff">30</span>,
</span></span><span style="display:flex;"><span>    fireInterval: <span style="color:#ae81ff">0.08f</span>,
</span></span><span style="display:flex;"><span>    reloadSeconds: <span style="color:#ae81ff">1.6f</span>,
</span></span><span style="display:flex;"><span>    usesBurstFire: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    isSuppressed: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>    projectileOverride: <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>    spreadDegrees: <span style="color:#ae81ff">2.5f</span>,
</span></span><span style="display:flex;"><span>    recoilKick: <span style="color:#ae81ff">0.35f</span>);
</span></span></code></pre></div><p>Named arguments may be all I need.</p>
<p>A builder earns a place when the setup is not just long, but <strong>stateful</strong>. Maybe burst fire requires a burst count. Maybe hitscan weapons cannot use projectile prefabs. Maybe reload time should default from magazine size unless explicitly overridden. Maybe a tool wants to assemble definitions from several data sources and validate once at the end.</p>
<p>At that point, a constructor is doing too much work at the wrong level. The builder gives construction its own workspace.</p>
<h2 id="a-builder-that-keeps-the-rules-in-one-place">A builder that keeps the rules in one place</h2>
<p>Here is a small version for a weapon definition. I kept it ordinary on purpose.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">WeaponDefinition</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">string</span> Id { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> Damage { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> FireInterval { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> MagazineSize { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> BurstCount { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> WeaponDefinition(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">string</span> id,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">int</span> damage,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">float</span> fireInterval,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">int</span> magazineSize,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">int</span> burstCount)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Id = id;
</span></span><span style="display:flex;"><span>        Damage = damage;
</span></span><span style="display:flex;"><span>        FireInterval = fireInterval;
</span></span><span style="display:flex;"><span>        MagazineSize = magazineSize;
</span></span><span style="display:flex;"><span>        BurstCount = burstCount;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">WeaponDefinitionBuilder</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">string</span> id;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> damage = <span style="color:#ae81ff">10</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">float</span> fireInterval = <span style="color:#ae81ff">0.2f</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> magazineSize = <span style="color:#ae81ff">12</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> burstCount = <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> WeaponDefinitionBuilder(<span style="color:#66d9ef">string</span> id)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.id = <span style="color:#66d9ef">string</span>.IsNullOrWhiteSpace(id)
</span></span><span style="display:flex;"><span>            ? <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ArgumentException(<span style="color:#e6db74">&#34;Weapon id is required.&#34;</span>, nameof(id))
</span></span><span style="display:flex;"><span>            : id;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> WeaponDefinitionBuilder WithDamage(<span style="color:#66d9ef">int</span> <span style="color:#66d9ef">value</span>)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        damage = <span style="color:#66d9ef">value</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> WeaponDefinitionBuilder WithFireInterval(<span style="color:#66d9ef">float</span> seconds)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        fireInterval = seconds;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> WeaponDefinitionBuilder WithMagazineSize(<span style="color:#66d9ef">int</span> <span style="color:#66d9ef">value</span>)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        magazineSize = <span style="color:#66d9ef">value</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> WeaponDefinitionBuilder AsBurstFire(<span style="color:#66d9ef">int</span> count)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        burstCount = count;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> WeaponDefinition Build()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (damage &lt;= <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(<span style="color:#e6db74">&#34;Damage must be positive.&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (fireInterval &lt;= <span style="color:#ae81ff">0f</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(<span style="color:#e6db74">&#34;Fire interval must be positive.&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (magazineSize &lt;= <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(<span style="color:#e6db74">&#34;Magazine size must be positive.&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (burstCount &lt;= <span style="color:#ae81ff">0</span> || burstCount &gt; magazineSize)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(<span style="color:#e6db74">&#34;Burst count must fit inside the magazine.&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> WeaponDefinition(
</span></span><span style="display:flex;"><span>            id,
</span></span><span style="display:flex;"><span>            damage,
</span></span><span style="display:flex;"><span>            fireInterval,
</span></span><span style="display:flex;"><span>            magazineSize,
</span></span><span style="display:flex;"><span>            burstCount);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This builder is boring. That is why I like it.</p>
<p>The final object is immutable from the outside. Defaults live in one place. Validation happens before the definition enters the rest of the game. The call site reads like intent instead of a bag of numbers:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> rifle = <span style="color:#66d9ef">new</span> WeaponDefinitionBuilder(<span style="color:#e6db74">&#34;burst_rifle&#34;</span>)
</span></span><span style="display:flex;"><span>    .WithDamage(<span style="color:#ae81ff">30</span>)
</span></span><span style="display:flex;"><span>    .WithFireInterval(<span style="color:#ae81ff">0.08f</span>)
</span></span><span style="display:flex;"><span>    .WithMagazineSize(<span style="color:#ae81ff">36</span>)
</span></span><span style="display:flex;"><span>    .AsBurstFire(<span style="color:#ae81ff">3</span>)
</span></span><span style="display:flex;"><span>    .Build();
</span></span></code></pre></div><p>If that construction appears in test code, import code, or a balancing tool, the readability win can be worth the extra type.</p>
<p>If it appears once in production code, it probably is not.</p>
<h2 id="fluent-does-not-mean-free">Fluent does not mean free</h2>
<p>Fluent APIs feel nice because they read in order. That is also why they are dangerous. A fluent chain can make bad abstractions look smooth.</p>
<p>This is the version to avoid:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">new</span> EnemyBuilder()
</span></span><span style="display:flex;"><span>    .WithBrain()
</span></span><span style="display:flex;"><span>    .WithMovement()
</span></span><span style="display:flex;"><span>    .WithHealth()
</span></span><span style="display:flex;"><span>    .WithLoot()
</span></span><span style="display:flex;"><span>    .WithAnimations()
</span></span><span style="display:flex;"><span>    .WithAudio()
</span></span><span style="display:flex;"><span>    .WithEverythingElse()
</span></span><span style="display:flex;"><span>    .Build();
</span></span></code></pre></div><p>That does not explain construction. It hides it.</p>
<p>A builder method should describe a meaningful choice. <code>WithDamage(20)</code> is useful. <code>WithHealth()</code> is suspicious unless there are clear options behind it. <code>WithEverythingElse()</code> is the builder waving a tiny red flag.</p>
<p>Fluent APIs also make debugging slightly less direct. A normal block of assignments lets you put a breakpoint on each line and inspect the object as it changes. A chain can still be debugged, but it encourages readers to treat setup as one expression. Simple chains are fine. It gets annoying when a chain starts doing real work.</p>
<p>So keep fluent builders boring:</p>
<ul>
<li>methods should set values, not perform expensive work</li>
<li><code>Build()</code> should be the place where creation happens</li>
<li>validation should be obvious and deterministic</li>
<li>method names should describe game concepts, not implementation steps</li>
<li>chains should stay short enough to scan</li>
</ul>
<p>If a builder needs logging, async calls, asset loading, scene searches, or service lookup inside every <code>With...</code> method, it is no longer a simple builder. It is a workflow object pretending to be a builder. Name it honestly or simplify it.</p>
<h2 id="unity-changes-the-tradeoff">Unity changes the tradeoff</h2>
<p>Unity code has a few construction traps that plain C# examples skip.</p>
<p>First, <code>MonoBehaviour</code> objects are not created with <code>new</code>. The engine owns their lifecycle. A builder that tries to construct components like normal C# objects will fight the platform.</p>
<p>Second, prefabs and <code>ScriptableObject</code> assets already solve a lot of configuration problems. If a designer needs to tune weapon data, enemy stats, or UI theme values, a serialized asset is usually a better source of truth than a hardcoded builder chain. Builders are code. Designers need data.</p>
<p>That does not make builders useless in Unity. It just narrows where they belong.</p>
<p>Good Unity fits include:</p>
<ul>
<li>building plain C# request objects before passing them to systems</li>
<li>assembling immutable runtime definitions from imported data</li>
<li>creating test fixtures without scene setup</li>
<li>constructing editor-tool commands</li>
<li>validating data during import or bake steps</li>
<li>wrapping prefab instantiation options into a clear request</li>
</ul>
<p>Poor Unity fits include:</p>
<ul>
<li>replacing prefabs with code-only construction for designer-owned content</li>
<li>hiding <code>Instantiate</code> calls and scene dependencies in fluent chains</li>
<li>building <code>MonoBehaviour</code> graphs as if they were normal objects</li>
<li>adding a builder around every small serializable class</li>
</ul>
<p>My rule is simple: let Unity assets handle data that should be edited as content. Use builders for code-owned construction rules.</p>
<h2 id="a-spawn-request-is-usually-better-than-a-spawn-builder">A spawn request is usually better than a spawn builder</h2>
<p>Spawning is where builders often look tempting and then become unnecessary.</p>
<p>A gameplay system might need this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">struct</span> SpawnRequest(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">string</span> EnemyId,
</span></span><span style="display:flex;"><span>    Vector3 Position,
</span></span><span style="display:flex;"><span>    Quaternion Rotation,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> Level,
</span></span><span style="display:flex;"><span>    Team Team);
</span></span></code></pre></div><p>The spawn system can validate the request and perform the work:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>spawner.Spawn(<span style="color:#66d9ef">new</span> SpawnRequest(
</span></span><span style="display:flex;"><span>    EnemyId: <span style="color:#e6db74">&#34;grunt&#34;</span>,
</span></span><span style="display:flex;"><span>    Position: spawnPoint.position,
</span></span><span style="display:flex;"><span>    Rotation: spawnPoint.rotation,
</span></span><span style="display:flex;"><span>    Level: wave.Level,
</span></span><span style="display:flex;"><span>    Team: Team.Enemy));
</span></span></code></pre></div><p>This is clean enough. Adding <code>new SpawnRequestBuilder().At(...).Facing(...).ForTeam(...).Build()</code> may not improve anything.</p>
<p>Use a builder only when the request has enough conditional rules to justify it. For example, a roguelike room generator might layer biome defaults, difficulty scaling, elite modifiers, and encounter tags before creating the final spawn plan. The builder is useful because it acts as a staging area for rules.</p>
<p>For one enemy at one transform, just pass the request.</p>
<h2 id="builders-are-excellent-in-tests">Builders are excellent in tests</h2>
<p>One place builders pay for themselves quickly is test setup.</p>
<p>Tests often need valid objects where only one value matters to the test. Without a builder, every test repeats the same boring constructor data. When the object changes, every test breaks for reasons unrelated to the behavior being tested.</p>
<p>A small test data builder fixes that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerStatsBuilder</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> maxHealth = <span style="color:#ae81ff">100</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> armor = <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">float</span> moveSpeed = <span style="color:#ae81ff">6f</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> PlayerStatsBuilder WithMaxHealth(<span style="color:#66d9ef">int</span> <span style="color:#66d9ef">value</span>)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        maxHealth = <span style="color:#66d9ef">value</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> PlayerStatsBuilder WithArmor(<span style="color:#66d9ef">int</span> <span style="color:#66d9ef">value</span>)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        armor = <span style="color:#66d9ef">value</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> PlayerStats Build()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> PlayerStats(maxHealth, armor, moveSpeed);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then a test can say what it cares about:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> stats = <span style="color:#66d9ef">new</span> PlayerStatsBuilder()
</span></span><span style="display:flex;"><span>    .WithMaxHealth(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    .Build();
</span></span></code></pre></div><p>This works because the builder hides irrelevant defaults, not important behavior. The test still tells the reader why this object is special.</p>
<p>The same idea works for gameplay simulations. If a combat test needs a poisoned enemy with low health and no armor, a fixture builder can make that setup obvious without dragging in every unrelated field.</p>
<p>Just do not let test builders leak into production API design automatically. Tests benefit from convenience. Production code benefits from clear boundaries.</p>
<h2 id="type-safe-staged-builders-are-rarely-worth-it">Type-safe staged builders are rarely worth it</h2>
<p>There is a more advanced version of the pattern where the type system forces calls to happen in a specific order. For example, you cannot call <code>Build()</code> until <code>WithOwner()</code> and <code>WithPrefab()</code> have both been called.</p>
<p>That can be valuable for public libraries or dangerous workflows. In game code, it is often overkill.</p>
<p>A staged builder tends to create several small interfaces or generic types:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>SpawnBuilder
</span></span><span style="display:flex;"><span>    .ForPrefab(prefab)   <span style="color:#75715e">// returns an interface that now allows WithOwner</span>
</span></span><span style="display:flex;"><span>    .WithOwner(player)   <span style="color:#75715e">// returns an interface that now allows At</span>
</span></span><span style="display:flex;"><span>    .At(position)        <span style="color:#75715e">// returns an interface that now allows Build</span>
</span></span><span style="display:flex;"><span>    .Build();
</span></span></code></pre></div><p>It is neat. It is also more machinery than most teams need.</p>
<p>For internal game systems, clear constructor requirements plus runtime validation are usually enough. The code is easier to change, and the error messages can mention the actual content problem instead of presenting a wall of generic type errors.</p>
<p>Use staged builders when invalid call order would be common, expensive, and hard to diagnose. Otherwise, keep the builder simple and let <code>Build()</code> validate.</p>
<h2 id="watch-for-hidden-mutable-state">Watch for hidden mutable state</h2>
<p>A builder is usually mutable. That is fine as long as it is treated as a short-lived construction object.</p>
<p>Problems start when builders are reused across calls:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ProjectileBuilder projectileBuilder = <span style="color:#66d9ef">new</span>(projectilePrefab);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> Projectile Fire(Vector3 direction)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> projectileBuilder
</span></span><span style="display:flex;"><span>        .WithDirection(direction)
</span></span><span style="display:flex;"><span>        .WithDamage(currentDamage)
</span></span><span style="display:flex;"><span>        .Build();
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That looks efficient, but now the builder is carrying state between shots. If one call sets an optional value and the next call does not reset it, the second projectile may inherit stale configuration. If multiple systems touch the same builder, the bug gets worse.</p>
<p>The safest default is to create builders near the call site and throw them away after <code>Build()</code>. They are tiny objects. If allocation matters in a hot path, that is a signal to avoid the builder there, not to keep a mutable builder singleton around.</p>
<p>Hot paths usually want explicit structs, precomputed data, pools, or command buffers. A fluent builder is primarily an API clarity tool, not a per-frame performance trick.</p>
<h2 id="a-quick-decision-checklist">A quick decision checklist</h2>
<p>Before adding a builder, ask these questions:</p>
<ol>
<li>Would an object initializer or named arguments be clear enough?</li>
<li>Does construction have validation rules that belong in one place?</li>
<li>Are there enough optional settings to make constructors noisy?</li>
<li>Does the final object benefit from being immutable after creation?</li>
<li>Will this setup appear in enough places to justify a reusable API?</li>
<li>Is this code-owned setup, or should it be designer-owned data?</li>
<li>Will the builder stay short, boring, and easy to debug?</li>
</ol>
<p>If the answer to the first question is yes, stop there. That is maintenance discipline.</p>
<p>If several later answers are yes, a builder can be a good fit.</p>
<h2 id="the-practical-takeaway">The practical takeaway</h2>
<p>I use builders to give complicated setup a small, honest workspace, not to make object creation look fancy.</p>
<p>Use a builder when construction has defaults, validation, optional combinations, or repeated setup noise that would otherwise leak into gameplay code. Keep the final object simple. Keep <code>Build()</code> responsible for validation and creation. Keep fluent methods boring. Do not use a builder to hide Unity lifecycle rules, asset loading, or scene wiring.</p>
<p>Most game objects do not need builders. The ones that do usually show the pain clearly: the constructor call is unreadable, the setup rules are scattered, or the tests are drowning in irrelevant fixture data.</p>
<p>I reach for the smallest tool that makes the construction rules visible. Sometimes that is a builder. Usually it is a good constructor, an object initializer, or a plain request type.</p>
<h2 id="references">References</h2>
<ul>
<li>Microsoft Learn: <a href="https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers">Object and collection initializers</a></li>
<li>Microsoft Learn: <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/required"><code>required</code> modifier</a></li>
<li>Microsoft Learn: <a href="https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/records">Records in C#</a></li>
<li>Martin Fowler: <a href="https://martinfowler.com/bliki/FluentInterface.html">Fluent Interface</a></li>
<li>Unity Manual: <a href="https://docs.unity3d.com/Manual/class-ScriptableObject.html">ScriptableObject</a></li>
<li>Robert Nystrom: <a href="https://gameprogrammingpatterns.com/prototype.html">Game Programming Patterns — Prototype</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Unity CLI and GitHub Actions: Run Edit Mode Tests on Every Push</title>
            <link>https://blog.jonmikeys.com/posts/unity-cli-github-actions-edit-mode-tests/</link>
            <pubDate>Sun, 23 Aug 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/unity-cli-github-actions-edit-mode-tests/</guid>
            <description>A practical Unity CI starter: install the Unity CLI, add a tiny Edit Mode smoke test, run it locally, then wire the same command into GitHub Actions.</description>
            <content type="html"><![CDATA[<p>Unity automation used to feel heavier than it should have for small game projects.</p>
<p>I do not mean “large studio build farm” automation. That is always going to have some weight behind it. I mean the first boring step: push a commit, run a few Edit Mode tests, keep the result somewhere visible, and stop letting broken utility code drift into the main branch unnoticed.</p>
<p>That first step should be cheap. It should be understandable. It should not require a whole DevOps personality transplant.</p>
<p>The Unity CLI makes that starter version much easier to justify. It gives us one command-line surface for project creation, editor installation, test execution, and eventually builds. GitHub Actions gives us a hosted runner, repository secrets, and artifact upload. Put those together and we get a small but useful CI loop: every push or pull request runs Unity tests without someone opening the editor by hand.</p>
<p>This post walks through that loop as a mini tutorial. I am not trying to build the perfect production pipeline. I want to get a real Unity project running a real Edit Mode test locally and in GitHub Actions, with code samples that are small enough to copy into a fresh repository.</p>
<p><img src="/images/posts/unity-cli-github-actions-edit-mode-tests/diagram.svg" alt="Unity CLI CI flow"></p>
<h2 id="the-smallest-useful-ci-loop">The smallest useful CI loop</h2>
<p>A good first Unity CI pipeline has four jobs, even if they all live in one GitHub Actions job at the start:</p>
<ol>
<li>install the Unity CLI</li>
<li>install or locate the editor version the project expects</li>
<li>activate a Unity license for the runner</li>
<li>run tests and upload the results</li>
</ol>
<p>That is enough to catch a surprising amount of damage. Edit Mode tests are especially good as a first target because they do not need a running scene, a graphics device, or a full player build. They are perfect for pure gameplay rules, data transforms, save/load helpers, stat formulas, targeting logic, validation code, inventory constraints, and the boring glue that quietly breaks when nobody is looking.</p>
<p>The lazy version is not to automate every possible thing immediately. The lazy version is to automate the cheapest check that would have caught yesterday’s mistake.</p>
<p>For most Unity projects, that is an Edit Mode smoke test.</p>
<h2 id="step-1-install-the-unity-cli">Step 1: install the Unity CLI</h2>
<p>Start by installing the Unity CLI using the command from Unity’s documentation for your operating system. The installer command changes by platform, so I avoid hardcoding it into project docs. Link the team to the official install page, then commit the commands you actually depend on after the CLI is installed.</p>
<p>Once it is available, check the version:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unity --version
</span></span></code></pre></div><p>Then ask the tool what it can do:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unity --help
</span></span><span style="display:flex;"><span>unity test --help
</span></span><span style="display:flex;"><span>unity editors --help
</span></span><span style="display:flex;"><span>unity projects --help
</span></span></code></pre></div><p>That habit matters. CLI tools grow. Flags move. Defaults change. A blog post or an old wiki page can get stale, but <code>--help</code> tells you what the installed version understands right now.</p>
<p>For the rest of this tutorial, assume the project wants one explicit editor version:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>export UNITY_VERSION<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;6000.0.0f1&#34;</span> <span style="color:#75715e"># replace with the version your project uses</span>
</span></span></code></pre></div><p>Do not let CI “use whatever is latest.” That is how a routine package update turns into a mystery failure. Pick an editor version, put it in one place, and update it deliberately.</p>
<h2 id="step-2-create-or-open-a-project-from-the-command-line">Step 2: create or open a project from the command line</h2>
<p>If you are starting from scratch, the project creation flow is roughly this shape:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unity templates list --editor-version <span style="color:#e6db74">&#34;</span>$UNITY_VERSION<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>unity projects create <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name CliCiSample <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --path . <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --editor-version <span style="color:#e6db74">&#34;</span>$UNITY_VERSION<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --template com.unity.template.urp-blank
</span></span></code></pre></div><p>Template identifiers vary, so list them first instead of guessing. For an existing project, skip creation and run commands from the repository root.</p>
<p>If you want the project visible in Unity Hub, add it explicitly:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unity projects add --path <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span><span style="color:#e6db74">/CliCiSample&#34;</span>
</span></span></code></pre></div><p>You can also open a project from the CLI:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unity projects open --path <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span><span style="color:#e6db74">/CliCiSample&#34;</span>
</span></span></code></pre></div><p>This is a small workflow improvement, but it also sets the tone for the pipeline. If a developer can run something locally from the terminal, CI can usually run the same thing in the cloud. If a step only exists as “click around in the editor until it works,” it is not automation yet.</p>
<h2 id="step-3-add-one-deliberately-boring-edit-mode-test">Step 3: add one deliberately boring Edit Mode test</h2>
<p>The first test should not be clever. Clever first tests usually turn into framework debugging. Start with a tiny smoke test that proves the test assembly is wired correctly.</p>
<p>Create this folder structure under <code>Assets</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Assets/
</span></span><span style="display:flex;"><span>  Tests/
</span></span><span style="display:flex;"><span>    EditMode/
</span></span><span style="display:flex;"><span>      Game.EditMode.Tests.asmdef
</span></span><span style="display:flex;"><span>      SmokeTests.cs
</span></span></code></pre></div><p>The assembly definition can be minimal:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Game.EditMode.Tests&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;rootNamespace&#34;</span>: <span style="color:#e6db74">&#34;Game.Tests&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;references&#34;</span>: [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;UnityEngine.TestRunner&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;UnityEditor.TestRunner&#34;</span>
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;includePlatforms&#34;</span>: [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;Editor&#34;</span>
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;excludePlatforms&#34;</span>: [],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;allowUnsafeCode&#34;</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;overrideReferences&#34;</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;precompiledReferences&#34;</span>: [],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;autoReferenced&#34;</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;defineConstraints&#34;</span>: [],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;versionDefines&#34;</span>: [],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;noEngineReferences&#34;</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then add the least exciting test possible:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> NUnit.Framework;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">namespace</span> Game.Tests
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SmokeTests</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">        [Test]</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Arithmetic_still_works()
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            Assert.AreEqual(<span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">1</span> + <span style="color:#ae81ff">1</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Yes, it is silly. That is why I like it.</p>
<p>This test is not meant to validate gameplay. It validates plumbing: the test assembly compiles, NUnit is available, the editor can load the project, and the CLI can execute Edit Mode tests. Once that works, replace the silly assertion with something useful.</p>
<p>For example, a real first gameplay test might check a pure formula:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> NUnit.Framework;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">namespace</span> Game.Tests
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DamageFormula</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">int</span> ApplyArmor(<span style="color:#66d9ef">int</span> rawDamage, <span style="color:#66d9ef">int</span> armor)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> System.Math.Max(<span style="color:#ae81ff">1</span>, rawDamage - armor);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DamageFormulaTests</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">        [Test]</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Armor_reduces_damage_but_never_below_one()
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            Assert.AreEqual(<span style="color:#ae81ff">7</span>, DamageFormula.ApplyArmor(rawDamage: <span style="color:#ae81ff">10</span>, armor: <span style="color:#ae81ff">3</span>));
</span></span><span style="display:flex;"><span>            Assert.AreEqual(<span style="color:#ae81ff">1</span>, DamageFormula.ApplyArmor(rawDamage: <span style="color:#ae81ff">10</span>, armor: <span style="color:#ae81ff">50</span>));
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is the kind of code Edit Mode tests are great at protecting. No scene. No prefab. No timing. Just a rule that should stay true.</p>
<h2 id="step-4-run-the-tests-locally-with-the-same-shape-ci-will-use">Step 4: run the tests locally with the same shape CI will use</h2>
<p>From the project root, run the tests through the Unity CLI:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unity test <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --project-path . <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --editor-version <span style="color:#e6db74">&#34;</span>$UNITY_VERSION<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --test-platform editmode <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --results-path test-results.xml <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --allow-install <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --timeout <span style="color:#ae81ff">600</span>
</span></span></code></pre></div><p>A few flags are doing useful work here.</p>
<p><code>--project-path .</code> makes the current directory the project. That is what the GitHub runner will do after checking out the repository.</p>
<p><code>--editor-version &quot;$UNITY_VERSION&quot;</code> keeps local and cloud behavior pinned to the same editor family.</p>
<p><code>--test-platform editmode</code> keeps this first job focused. Play Mode tests and builds can come later.</p>
<p><code>--results-path test-results.xml</code> writes a file that CI can upload as an artifact.</p>
<p><code>--allow-install</code> is convenient on fresh machines because the CLI can install the requested editor if it is missing. Whether you want that in production CI depends on how much control you want over cold-start time. For a starter pipeline, it is acceptable. For a mature pipeline, caching and pre-baked runners may be better.</p>
<p><code>--timeout 600</code> keeps a wedged test run from consuming the runner forever. Ten minutes is generous for a tiny Edit Mode suite, but it is a sane first guardrail.</p>
<p>Add the test result file to <code>.gitignore</code>:</p>
<pre tabindex="0"><code class="language-gitignore" data-lang="gitignore"># Unity CLI test output
test-results*.xml
test-results*.json
</code></pre><p>Generated test reports are build artifacts, not source code. Commit the tests, not yesterday’s result file.</p>
<h2 id="step-5-create-the-github-secrets">Step 5: create the GitHub secrets</h2>
<p>A GitHub-hosted runner needs to activate Unity before it can run editor-backed work. Store those values as repository secrets, not plain workflow text.</p>
<p>Use these names in the workflow below:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>UNITY_EMAIL
</span></span><span style="display:flex;"><span>UNITY_PASSWORD
</span></span><span style="display:flex;"><span>UNITY_SERIAL
</span></span></code></pre></div><p>In GitHub, add them under:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Repository → Settings → Secrets and variables → Actions → New repository secret
</span></span></code></pre></div><p>If you are using a paid Unity seat, pay attention to license seats. CI runners are machines too from the license server’s point of view. If a job activates a seat and never returns it, the next run may fail in a way that looks unrelated to the code change.</p>
<p>That is why the workflow below returns the license in an <code>always()</code> step. Boring cleanup is not optional here. It is the difference between useful automation and a tiny license leak machine.</p>
<h2 id="step-6-add-the-github-actions-workflow">Step 6: add the GitHub Actions workflow</h2>
<p>Create this file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>.github/workflows/unity-edit-mode-tests.yml
</span></span></code></pre></div><p>Then start with one job:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">Unity Edit Mode Tests</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">push</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>: [<span style="color:#e6db74">&#34;main&#34;</span>, <span style="color:#e6db74">&#34;master&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pull_request</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>: [<span style="color:#e6db74">&#34;main&#34;</span>, <span style="color:#e6db74">&#34;master&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow_dispatch</span>:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">UNITY_VERSION</span>: <span style="color:#e6db74">&#34;6000.0.0f1&#34;</span> <span style="color:#75715e"># replace with your project version</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">TEST_RESULTS_XML</span>: <span style="color:#ae81ff">test-results.xml</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">TEST_RESULTS_JSON</span>: <span style="color:#ae81ff">test-results.json</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">edit-mode-tests</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Edit Mode Tests</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">timeout-minutes</span>: <span style="color:#ae81ff">30</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Check out repository</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v6</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Install Unity CLI</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">shell</span>: <span style="color:#ae81ff">bash</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          set -euo pipefail
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          curl -fsSL &#34;https://services.api.unity.com/unity/editor/v1/download/cli/linux&#34; | bash
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          unity --version</span>          
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Install Unity Editor</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">shell</span>: <span style="color:#ae81ff">bash</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          set -euo pipefail
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          unity install \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --version &#34;$UNITY_VERSION&#34; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --yes \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --accept-license</span>          
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Activate Unity license</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">shell</span>: <span style="color:#ae81ff">bash</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          set -euo pipefail
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          EDITOR_PATH=&#34;$(unity editors --installed --json \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            | python3 -c &#39;import json,sys; data=json.load(sys.stdin); print(data[0][&#34;path&#34;])&#39;)&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          &#34;$EDITOR_PATH/Editor/Unity&#34; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            -batchmode \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            -nographics \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            -quit \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            -serial &#34;${{ secrets.UNITY_SERIAL }}&#34; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            -username &#34;${{ secrets.UNITY_EMAIL }}&#34; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            -password &#34;${{ secrets.UNITY_PASSWORD }}&#34;</span>          
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Run Edit Mode tests</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">shell</span>: <span style="color:#ae81ff">bash</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          set -euo pipefail
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          unity test \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --project-path . \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --editor-version &#34;$UNITY_VERSION&#34; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --test-platform editmode \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --results-path &#34;$TEST_RESULTS_XML&#34; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --results-format xml \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --allow-install \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --timeout 600 \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --no-graphics
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          unity test \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --project-path . \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --editor-version &#34;$UNITY_VERSION&#34; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --test-platform editmode \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --results-path &#34;$TEST_RESULTS_JSON&#34; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --results-format json \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --allow-install \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --timeout 600 \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --no-graphics</span>          
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Upload test results</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">if</span>: <span style="color:#ae81ff">always()</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/upload-artifact@v4</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#ae81ff">unity-edit-mode-test-results</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">path</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            ${{ env.TEST_RESULTS_XML }}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            ${{ env.TEST_RESULTS_JSON }}</span>            
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">if-no-files-found</span>: <span style="color:#ae81ff">warn</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Return Unity license</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">if</span>: <span style="color:#ae81ff">always()</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">shell</span>: <span style="color:#ae81ff">bash</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          set -euo pipefail
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          EDITOR_PATH=&#34;$(unity editors --installed --json \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            | python3 -c &#39;import json,sys; data=json.load(sys.stdin); print(data[0][&#34;path&#34;])&#39;)&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          &#34;$EDITOR_PATH/Editor/Unity&#34; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            -batchmode \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            -nographics \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            -quit \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            -returnlicense \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            -username &#34;${{ secrets.UNITY_EMAIL }}&#34; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            -password &#34;${{ secrets.UNITY_PASSWORD }}&#34;</span>          
</span></span></code></pre></div><p>I keep this as one file and one job on purpose. No reusable actions. No custom Docker image. No cache. No matrix. Not yet.</p>
<p>The first version of a CI workflow should be boring enough that anyone on the project can read it. Once it works, then optimize.</p>
<h2 id="the-important-parts-of-the-workflow">The important parts of the workflow</h2>
<p>The trigger block runs on pushes and pull requests to <code>main</code> or <code>master</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">push</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>: [<span style="color:#e6db74">&#34;main&#34;</span>, <span style="color:#e6db74">&#34;master&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pull_request</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>: [<span style="color:#e6db74">&#34;main&#34;</span>, <span style="color:#e6db74">&#34;master&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow_dispatch</span>:
</span></span></code></pre></div><p><code>workflow_dispatch</code> is worth keeping. It gives you a manual “run this again” button in GitHub Actions. It helps when debugging runner issues without creating junk commits.</p>
<p>The <code>env</code> block keeps the repeated strings in one place:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">UNITY_VERSION</span>: <span style="color:#e6db74">&#34;6000.0.0f1&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">TEST_RESULTS_XML</span>: <span style="color:#ae81ff">test-results.xml</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">TEST_RESULTS_JSON</span>: <span style="color:#ae81ff">test-results.json</span>
</span></span></code></pre></div><p>This only removes the easiest typo source. If the version appears five times, someone will update four of them.</p>
<p>The checkout step is standard GitHub Actions plumbing:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Check out repository</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v6</span>
</span></span></code></pre></div><p>Without it, the runner has an empty workspace. Obvious, yes, and still easy to forget when writing the first workflow by hand.</p>
<p>The Unity CLI install step is the cloud equivalent of installing it locally:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Install Unity CLI</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">shell</span>: <span style="color:#ae81ff">bash</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    set -euo pipefail
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    curl -fsSL &#34;https://services.api.unity.com/unity/editor/v1/download/cli/linux&#34; | bash
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    unity --version</span>    
</span></span></code></pre></div><p>If Unity changes the install URL, use the current URL from the official docs. The rest of the workflow should still hold its shape.</p>
<p>The editor install step pins the editor version:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ae81ff">unity install \</span>
</span></span><span style="display:flex;"><span>  --<span style="color:#ae81ff">version &#34;$UNITY_VERSION&#34; \</span>
</span></span><span style="display:flex;"><span>  --<span style="color:#66d9ef">yes</span> <span style="color:#ae81ff">\</span>
</span></span><span style="display:flex;"><span>  --<span style="color:#ae81ff">accept-license</span>
</span></span></code></pre></div><p>This can be slow on a cold runner, and that is fine for a first pass. If it becomes painful, the next lazy improvement is caching or a custom runner image, not rewriting the whole pipeline.</p>
<p>The test step runs exactly the kind of command we already ran locally:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ae81ff">unity test \</span>
</span></span><span style="display:flex;"><span>  --<span style="color:#ae81ff">project-path . \</span>
</span></span><span style="display:flex;"><span>  --<span style="color:#ae81ff">editor-version &#34;$UNITY_VERSION&#34; \</span>
</span></span><span style="display:flex;"><span>  --<span style="color:#ae81ff">test-platform editmode \</span>
</span></span><span style="display:flex;"><span>  --<span style="color:#ae81ff">results-path &#34;$TEST_RESULTS_XML&#34; \</span>
</span></span><span style="display:flex;"><span>  --<span style="color:#ae81ff">results-format xml \</span>
</span></span><span style="display:flex;"><span>  --<span style="color:#ae81ff">allow-install \</span>
</span></span><span style="display:flex;"><span>  --<span style="color:#ae81ff">timeout 600 \</span>
</span></span><span style="display:flex;"><span>  --<span style="color:#66d9ef">no</span>-<span style="color:#ae81ff">graphics</span>
</span></span></code></pre></div><p>I want that mirroring. Local and CI should not feel like two separate worlds. If CI fails, you want a developer to copy the command, run it locally, and reproduce the problem.</p>
<h2 id="a-useful-second-test-protect-a-real-rule">A useful second test: protect a real rule</h2>
<p>Once the smoke test passes in CI, delete it or leave it alone and add one test that protects real game behavior.</p>
<p>Here is a small inventory example. It is still pure C#, so it belongs in Edit Mode tests:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">namespace</span> Game.Inventory
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">InventorySlot</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">string</span> ItemId { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">set</span>; } = <span style="color:#e6db74">&#34;&#34;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> Count { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> Capacity { <span style="color:#66d9ef">get</span>; }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> InventorySlot(<span style="color:#66d9ef">int</span> capacity)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            Capacity = capacity &gt; <span style="color:#ae81ff">0</span> ? capacity : <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> Add(<span style="color:#66d9ef">string</span> itemId, <span style="color:#66d9ef">int</span> amount)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">string</span>.IsNullOrWhiteSpace(itemId) || amount &lt;= <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span> amount;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> (Count &gt; <span style="color:#ae81ff">0</span> &amp;&amp; ItemId != itemId)
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span> amount;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            ItemId = itemId;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> accepted = System.Math.Min(amount, Capacity - Count);
</span></span><span style="display:flex;"><span>            Count += accepted;
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> amount - accepted;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>And the tests:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> Game.Inventory;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> NUnit.Framework;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">namespace</span> Game.Tests
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">InventorySlotTests</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">        [Test]</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Add_fills_empty_slot_and_returns_leftovers()
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> slot = <span style="color:#66d9ef">new</span> InventorySlot(capacity: <span style="color:#ae81ff">10</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> leftover = slot.Add(<span style="color:#e6db74">&#34;potion&#34;</span>, amount: <span style="color:#ae81ff">14</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            Assert.AreEqual(<span style="color:#e6db74">&#34;potion&#34;</span>, slot.ItemId);
</span></span><span style="display:flex;"><span>            Assert.AreEqual(<span style="color:#ae81ff">10</span>, slot.Count);
</span></span><span style="display:flex;"><span>            Assert.AreEqual(<span style="color:#ae81ff">4</span>, leftover);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">        [Test]</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Add_rejects_different_item_when_slot_is_occupied()
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> slot = <span style="color:#66d9ef">new</span> InventorySlot(capacity: <span style="color:#ae81ff">10</span>);
</span></span><span style="display:flex;"><span>            slot.Add(<span style="color:#e6db74">&#34;potion&#34;</span>, amount: <span style="color:#ae81ff">3</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> leftover = slot.Add(<span style="color:#e6db74">&#34;bomb&#34;</span>, amount: <span style="color:#ae81ff">2</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            Assert.AreEqual(<span style="color:#e6db74">&#34;potion&#34;</span>, slot.ItemId);
</span></span><span style="display:flex;"><span>            Assert.AreEqual(<span style="color:#ae81ff">3</span>, slot.Count);
</span></span><span style="display:flex;"><span>            Assert.AreEqual(<span style="color:#ae81ff">2</span>, leftover);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>For the first pipeline, this beats a scene-heavy integration test. It runs quickly, it fails clearly, and it protects an actual rule. If someone changes stacking behavior, the failure points at the rule instead of giving you a vague “player prefab did not initialize” problem.</p>
<h2 id="common-mistakes-worth-avoiding">Common mistakes worth avoiding</h2>
<p>The first mistake is checking in generated test results. Test output belongs in artifacts. Source control should hold tests and workflow files.</p>
<p>The second mistake is hiding the Unity version. Put it in the workflow <code>env</code> block, and ideally mirror it in project documentation. Better yet, keep a tiny <code>ci/unity-version.txt</code> file and have both scripts read it. I treat that as a follow-up once the basic workflow is stable.</p>
<p>The third mistake is not returning the license. The cleanup step should use <code>if: always()</code> because failures are exactly when cleanup gets skipped by accident.</p>
<p>The fourth mistake is starting with a full build matrix. Android, Windows, macOS, WebGL, Play Mode tests, asset validation, package publishing — all useful eventually. None belong on day one. One passing Edit Mode job is better than a giant workflow nobody trusts.</p>
<p>The fifth mistake is making CI-only commands. If a developer cannot run the same test command locally, debugging gets slower. Keep the local and cloud commands as similar as possible.</p>
<h2 id="where-i-would-take-it-next">Where I would take it next</h2>
<p>After Edit Mode tests are stable, I would add one improvement at a time.</p>
<p>First, split local commands into scripts:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>scripts/test-editmode.sh
</span></span><span style="display:flex;"><span>scripts/install-unity-editor.sh
</span></span></code></pre></div><p>Then call those scripts from GitHub Actions. That keeps the workflow short and gives developers the same entry points locally.</p>
<p>Second, add Play Mode tests only for systems that genuinely need engine behavior. Do not move pure logic into Play Mode tests just because it lives in a Unity project.</p>
<p>Third, add a build job on release branches:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unity build --help
</span></span></code></pre></div><p>Start by reading the CLI help for your installed version, then create one build target. Do not build every platform until one platform is boring.</p>
<p>Fourth, parse the XML or JSON results into a small summary. Artifact upload is enough at first, but a readable failure summary in the GitHub Actions log saves time once the test suite grows.</p>
<h2 id="the-rule-i-keep">The rule I keep</h2>
<p>Unity CI does not have to start as a complicated build farm.</p>
<p>Start with one command that runs one Edit Mode test suite. Run it locally. Put the same shape into GitHub Actions. Upload the result. Return the license. Keep the workflow boring until boring stops being enough.</p>
<p>That gives the project a real safety rail without asking the whole team to become infrastructure specialists. And once that rail exists, every small pure-code test you add gets checked automatically on every push.</p>
<p>I like that kind of automation because it is small, visible, and annoying enough to catch mistakes before players do.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://docs.unity.com/en-us/unity-version-control/unity-cli">Unity CLI documentation</a></li>
<li><a href="https://docs.unity3d.com/Packages/com.unity.test-framework@latest">Unity Test Framework documentation</a></li>
<li><a href="https://docs.github.com/en/actions">GitHub Actions documentation</a></li>
<li><a href="https://github.com/actions/upload-artifact">GitHub Actions artifact upload action</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Memory Arenas in Game Code: Fast Scratch Space, Not a General Allocation Strategy</title>
            <link>https://blog.jonmikeys.com/posts/memory-arenas-in-game-code/</link>
            <pubDate>Tue, 18 Aug 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/memory-arenas-in-game-code/</guid>
            <description>How I use arena allocators for frame-local scratch data, why they beat per-object frees, and why they are not a replacement for pooling or the GC.</description>
            <content type="html"><![CDATA[<p>I reach for a memory arena when I know two things up front: the data is temporary, and the temporary data all dies together.</p>
<p>The scope is narrow on purpose. An arena is not a magic optimization bucket. It is a very specific answer to a very specific lifetime problem: allocate a bunch of scratch data quickly, use it for one phase of work, then throw the whole thing away in one shot.</p>
<p>This fits for game code more often than people expect. Frame-local simulation data, temporary pathfinding buffers, animation build steps, UI layout scratch, packet decoding, command buffering, visibility lists, import pipelines — all of those can be arena-shaped if the lifetime is tidy.</p>
<p>The moment the lifetime stops being tidy, the arena stops being elegant.</p>
<p><img src="/images/posts/memory-arenas-in-game-code/diagram.svg" alt="Arena lifetime diagram"></p>
<h2 id="what-an-arena-actually-is">What an arena actually is</h2>
<p>The core idea is simple.</p>
<p>An arena starts with one big block of memory. Allocation means moving a cursor forward. Deallocation is usually not per-object cleanup. It is a reset.</p>
<p>That gives me a few useful properties:</p>
<ul>
<li>allocation is cheap</li>
<li>lifetime is obvious</li>
<li>memory tends to stay contiguous</li>
<li>I do not pay bookkeeping for every tiny object</li>
</ul>
<p>Most arena implementations are just a bump pointer plus a reset point.</p>
<p>That is the trick.</p>
<p>The reason it works so well is that many game workloads are phase based. I do some work, I need temporary storage while I do it, and once the phase ends I do not care about the intermediate values anymore. If I can say that out loud, I usually have an arena candidate.</p>
<p>If I cannot say that out loud, I usually do not.</p>
<h2 id="why-game-code-likes-arenas">Why game code likes arenas</h2>
<p>Games create a lot of temporary structure.</p>
<p>A pathfinding query needs an open list, a closed list, a cost table, and maybe some extra scratch to reconstruct a path. A UI layout pass may need temporary rectangles, text measurements, and a few layout stacks. An animation build step may need a batch of baked curves or pose data. A visibility pass may need a short-lived list of candidates before the real filter runs.</p>
<p>Arenas help because they match the shape of the work.</p>
<p>I do not care about those objects one by one. I care that the whole batch belongs to one step in the pipeline. When the step finishes, the arena can be rewound and reused.</p>
<p>That is the real value. Not raw speed, although speed often follows. The bigger win is that the code starts telling the truth about its lifetime.</p>
<p>A thousand individual frees are usually a smell in game code. They often mean I have let a temporary phase leak into the rest of the system.</p>
<h2 id="the-lifetime-question-is-the-real-design-question">The lifetime question is the real design question</h2>
<p>Every time I consider an arena, I ask one boring question:</p>
<blockquote>
<p>Does this data die at the same time as the thing that created it?</p>
</blockquote>
<p>If the answer is yes, an arena is probably a good fit.</p>
<p>If the answer is “mostly” or “sometimes” or “I guess so,” I slow down. Arena allocation gets messy when objects have mixed lifetimes. The moment some entries must survive and others must disappear, the clean reset story breaks down.</p>
<p>That is where a lot of people get burned.</p>
<p>They choose an arena because they want to avoid fragmentation, but then they start keeping long-lived references into arena memory, or they tuck one persistent object inside a sea of scratch allocations, and suddenly the nice simple rule has turned into a bug factory.</p>
<p>Arenas want phase boundaries. They do not like exceptions.</p>
<h2 id="arena-versus-pool-versus-garbage-collection">Arena versus pool versus garbage collection</h2>
<p>I do not treat these as competing religions. They solve different problems.</p>
<p><strong>Garbage collection</strong> is fine when the runtime owns the lifecycle and the allocation rate is reasonable. The problem in game code is not that GC exists. The problem is that a hot path can generate enough pressure to make collection visible at the wrong time.</p>
<p><strong>Object pools</strong> are useful when I want to reuse expensive objects individually. A pool is a good fit when I keep renting the same shape over and over, but the lifetime is not perfectly synchronized across the whole batch.</p>
<p><strong>Arenas</strong> are for phase-local scratch data. They are simpler than a pool when the lifetime is batch-shaped.</p>
<p>I keep that distinction separate.</p>
<p>If I need a reusable bullet object, a pool may be the right answer.
If I need temporary collision candidates for a single simulation step, an arena is often better.
If I need ordinary game state with rich behavior and long-lived ownership, the managed heap or normal engine objects may still be the right tool.</p>
<p>I want the smallest tool that tells the truth about the lifetime.</p>
<h2 id="unity-already-gives-me-arena-like-tools">Unity already gives me arena-like tools</h2>
<p>In Unity, I do not need to invent the concept from scratch to use the idea.</p>
<p>Unity’s allocator docs split unmanaged memory into three useful lifetime buckets:</p>
<ul>
<li><code>Allocator.Temp</code> for very short-lived work</li>
<li><code>Allocator.TempJob</code> for short-lived work that can cross into jobs</li>
<li><code>Allocator.Persistent</code> for long-lived allocations</li>
</ul>
<p>That model already has arena-shaped thinking in it.</p>
<p><code>Allocator.Temp</code> is the purest example. It is the “use this now and throw it away immediately” bucket. Unity documents it as the fastest allocator, which is exactly what I would expect from a scratch-space design.</p>
<p><code>Allocator.TempJob</code> is still temporary, but the lifetime is long enough to survive a job boundary.</p>
<p><code>Allocator.Persistent</code> is the opposite end of the spectrum. It is not for scratch work. It is for data that sticks around.</p>
<p>So when I work in Unity, I usually ask whether I can make the engine’s own lifetime buckets do the job before I reach for something custom. That is the lazy answer, and the lazy answer is often the correct one.</p>
<h2 id="array-pools-are-useful-but-they-are-not-arenas">Array pools are useful, but they are not arenas</h2>
<p>This is where people mix terms and then wonder why the code feels odd.</p>
<p><code>.NET</code> gives me <code>ArrayPool&lt;T&gt;</code>, and Microsoft’s docs are clear that renting and returning buffers is useful when arrays are created and destroyed frequently because it reduces garbage collector pressure.</p>
<p>That helps, but it is not the same thing as an arena.</p>
<p>A pool recycles individual buffers.
An arena usually resets the whole batch at once.</p>
<p>The ownership model changes:</p>
<ul>
<li>pool: I rent one thing, use it, return it</li>
<li>arena: I allocate many things, use them together, reset the whole region</li>
</ul>
<p>Pools are good when I need reuse but do not have a single clean phase boundary.
Arenas are good when I do have that boundary.</p>
<p>If I use the wrong one, I usually end up fighting the lifecycle model instead of benefiting from it.</p>
<h2 id="a-small-arena-shape-in-c">A small arena shape in C#</h2>
<p>For a lot of game tools, I do not need a giant allocator framework. I need one bounded scratch buffer with an explicit reset.</p>
<p>This is the shape I mean:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Buffers;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">FrameScratch</span> : IDisposable
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">byte</span>[] _buffer;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> _offset;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> FrameScratch(<span style="color:#66d9ef">int</span> size)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _buffer = ArrayPool&lt;<span style="color:#66d9ef">byte</span>&gt;.Shared.Rent(size);
</span></span><span style="display:flex;"><span>        _offset = <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Span&lt;<span style="color:#66d9ef">byte</span>&gt; Allocate(<span style="color:#66d9ef">int</span> bytes)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (bytes &lt; <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ArgumentOutOfRangeException(nameof(bytes));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (_offset + bytes &gt; _buffer.Length)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(<span style="color:#e6db74">&#34;FrameScratch exhausted&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> slice = _buffer.AsSpan(_offset, bytes);
</span></span><span style="display:flex;"><span>        _offset += bytes;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> slice;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Reset()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _offset = <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Dispose()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        ArrayPool&lt;<span style="color:#66d9ef">byte</span>&gt;.Shared.Return(_buffer);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is not a universal allocator. It is intentionally limited.</p>
<p>The ceiling is obvious: one buffer, one cursor, one reset point. If I need typed storage, alignment, nested lifetimes, or concurrent writers, I should build or use something stronger. But for phase-local scratch data, this is often enough.</p>
<p>The important part is that the code advertises its lifetime.</p>
<h2 id="where-i-use-it">Where I use it</h2>
<p>I like arenas for work that already has a natural batch.</p>
<p>A few examples:</p>
<ul>
<li>pathfinding queries that need temporary search data</li>
<li>procedural generation passes that build intermediate structures</li>
<li>text layout and UI measurement passes</li>
<li>importer and baker tools that chew through a lot of temporary data</li>
<li>gameplay simulation steps that gather candidates, then discard them all together</li>
<li>network decode or command parsing where the temp state is only needed while processing one packet</li>
</ul>
<p>This is the pattern I look for: a burst of temporary data, one owner, one phase, one reset.</p>
<p>If the code starts storing references to the scratch data somewhere else, I treat that as a warning sign. An arena is not a way to avoid ownership. It is a way to make ownership simpler.</p>
<h2 id="where-i-do-not-use-it">Where I do not use it</h2>
<p>Arenas are a bad fit when the data has mixed lifetimes or needs individual disposal.</p>
<p>I avoid them when:</p>
<ul>
<li>objects survive across many frames</li>
<li>the data is part of persistent game state</li>
<li>different entries expire at different times</li>
<li>I need to delete arbitrary items in the middle of the set</li>
<li>I want the allocation boundary to be invisible</li>
<li>the memory must outlive the current phase but I keep pretending it will not</li>
</ul>
<p>The last one is the classic mistake.</p>
<p>An arena is not a shortcut around thinking about ownership. It is a way to make the ownership rule brutally clear.</p>
<p>If the rule is not clear, the arena is the wrong tool.</p>
<h2 id="the-two-mistakes-i-see-most-often">The two mistakes I see most often</h2>
<h3 id="1-using-an-arena-for-everything">1. Using an arena for everything</h3>
<p>This is the classic overcorrection.</p>
<p>Someone discovers arenas and starts feeding them every allocation in the project. It usually creates a worse mess than the garbage collector ever did. Long-lived state, temporary scratch, cached assets, and ephemeral buffers all start sharing one mechanism that was only designed for one kind of lifetime.</p>
<p>That is denial with better performance numbers.</p>
<h3 id="2-forgetting-the-reset-boundary">2. Forgetting the reset boundary</h3>
<p>The other mistake is subtler.</p>
<p>The arena works beautifully in a local test, and then someone forgets to reset it at the end of the frame or phase. Memory usage climbs. The code still “works,” which makes the bug annoying to catch. It only looks like a leak because it is one in practice.</p>
<p>An arena without a clear reset point is just a buffer that gets less useful over time.</p>
<h2 id="what-i-look-for-before-i-introduce-one">What I look for before I introduce one</h2>
<p>Before I add an arena, I want three things to be true:</p>
<ol>
<li>the lifetime is short and obvious</li>
<li>the data is temporary and batch-shaped</li>
<li>the code has a natural place to reset</li>
</ol>
<p>If those three are true, the arena usually pulls its weight.</p>
<p>If they are not true, I usually do something simpler.</p>
<p>That might mean a normal <code>List&lt;T&gt;</code>, a pool, Unity’s built-in allocator lifetime buckets, or just leaving the code alone because the allocation cost is not actually the bottleneck.</p>
<p>Leaving code alone is sometimes the useful habit.</p>
<p>Not every temporary allocation problem is big enough to deserve a custom allocator.</p>
<h2 id="the-practical-rule-i-keep">The practical rule I keep</h2>
<p>My rule is simple:</p>
<ul>
<li>if the data belongs to one phase, use a phase-local allocation strategy</li>
<li>if the data belongs to one object, let the object own it</li>
<li>if the data belongs to many unrelated lifetimes, do not force an arena on it</li>
</ul>
<p>This keeps me from overengineering the allocator and underengineering the ownership model.</p>
<p>Arenas work best when they stay boring. Once they become clever, I usually reach for a simpler ownership model.</p>
<h2 id="the-rule-i-keep">The rule I keep</h2>
<p>I use memory arenas when I want fast scratch space with a hard reset boundary.</p>
<p>They are not a replacement for the GC, and they are not a fancy pool. They are a lifetime tool. If I can say exactly when the data dies, the arena is a strong candidate. If I cannot, I leave the memory model alone and pick something simpler.</p>
<p>I pick the allocator that matches the lifetime of the work.</p>
<h2 id="references">References</h2>
<ul>
<li>Unity Manual, <a href="https://docs.unity3d.com/Manual/performance-optimizing-code-managed-memory.html">Optimizing your code for managed memory</a></li>
<li>Unity Collections, <a href="https://docs.unity3d.com/Packages/com.unity.collections@2.5/manual/allocator-overview.html">Allocator overview</a></li>
<li>Microsoft Learn, <a href="https://learn.microsoft.com/en-us/dotnet/api/system.buffers.arraypool-1">ArrayPool<!-- raw HTML omitted --> Class</a></li>
<li>Dylan Falconer, <a href="https://www.bytesbeneath.com/p/the-arena-custom-memory-allocators">The Arena - Custom Memory Allocators in C</a></li>
<li>Karl Zylinski, <a href="https://zylinski.se/posts/dynamic-arrays-and-arenas/">Mistakes and cool things to do with arena allocators</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>API Programming Through HTTP: The Rules I Lean on in Game Code</title>
            <link>https://blog.jonmikeys.com/posts/api-programming-through-http/</link>
            <pubDate>Fri, 14 Aug 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/api-programming-through-http/</guid>
            <description>A practical guide to calling HTTP APIs from game code without making retries, auth, or failure handling a mess.</description>
            <content type="html"><![CDATA[<p>HTTP API work looks simple when the demo is small.</p>
<p>I send a request, get some JSON back, and move on with my day. Then the project grows a login flow, cloud saves, entitlement checks, matchmaking, inventory, telemetry, or a live-ops tool, and the “simple” layer turns into the place where every failure mode meets the rest of the game.</p>
<p>The trouble usually starts when I treat HTTP like a transport detail instead of a contract.</p>
<p>What I want from an API is boring in the best way: predictable requests, predictable responses, and error handling that does not punish me every time the network hiccups.</p>
<p><img src="/images/posts/api-programming-through-http/diagram.svg" alt="Api Programming Through Http diagram"></p>
<h2 id="the-shape-of-the-problem">The shape of the problem</h2>
<p>HTTP is useful because it gives me a shared language.</p>
<p>Methods tell me what kind of action I am making. Status codes tell me what happened. Headers carry metadata. Bodies carry the actual data.</p>
<p>Obvious, but a lot of game code ignores half of it and then wonders why retries are scary or debugging is awful.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Game code
</span></span><span style="display:flex;"><span>   ↓
</span></span><span style="display:flex;"><span>API client wrapper
</span></span><span style="display:flex;"><span>   ↓ adds auth, timeout, logging, correlation id
</span></span><span style="display:flex;"><span>HTTP request
</span></span><span style="display:flex;"><span>   ↓
</span></span><span style="display:flex;"><span>API service
</span></span><span style="display:flex;"><span>   ↓
</span></span><span style="display:flex;"><span>status code + headers + JSON body
</span></span><span style="display:flex;"><span>   ↓
</span></span><span style="display:flex;"><span>DTO validation
</span></span><span style="display:flex;"><span>   ↓
</span></span><span style="display:flex;"><span>game state
</span></span></code></pre></div><p>I care about the wrapper in the middle. I want one place that knows how requests are built, how responses are decoded, and which failures should be retried. The rest of the game should not care whether I used <code>HttpClient</code>, <code>UnityWebRequest</code>, or a platform SDK under the hood.</p>
<h2 id="start-with-the-contract-not-the-code">Start with the contract, not the code</h2>
<p>The first mistake I see is starting from the client implementation and letting the API shape fall out later.</p>
<p>That can survive a prototype. It does not survive a game that keeps shipping updates.</p>
<p>I get better results when I decide these things first:</p>
<ul>
<li>what resource the endpoint represents</li>
<li>whether the operation is a read or a write</li>
<li>which fields are required, optional, or server-owned</li>
<li>what a successful response looks like</li>
<li>what the client should do on failure</li>
</ul>
<p>I do <strong>not</strong> need religious REST purity to do this well. I just need the method and the path to match the intent.</p>
<p>A read should look like a read. A create should look like a create. A destructive action should not hide behind a query string because it was convenient once.</p>
<p>I usually aim for endpoints like this:</p>
<ul>
<li><code>GET /players/123/profile</code> to read data</li>
<li><code>POST /players/123/inventory/consume</code> to perform a command</li>
<li><code>PUT /players/123/settings</code> when I am replacing a known resource</li>
<li><code>PATCH /players/123/settings</code> when I am changing a small subset</li>
<li><code>DELETE /players/123/cosmetics/hat-01</code> when I am removing a resource</li>
</ul>
<p>I am not trying to win style points. I am trying to make the API readable six months later, when I have forgotten the implementation and only the bug report remains.</p>
<h2 id="http-methods-are-a-hint-about-retry-safety">HTTP methods are a hint about retry safety</h2>
<p>This is where HTTP starts paying rent.</p>
<p>The method is not just syntax. It is a promise about how the request behaves when I have to repeat it.</p>
<p>The important distinction is this:</p>
<ul>
<li><strong>safe</strong> methods should not change server state</li>
<li><strong>idempotent</strong> methods should have the same effect if repeated</li>
</ul>
<p>In practice, that means a <code>GET</code> is usually safe to retry, a <code>PUT</code> or <code>DELETE</code> is usually safe to retry, and a <code>POST</code> is the one I treat with caution unless I add deduplication on purpose.</p>
<p>Games make this painful because networks fail at annoying times.</p>
<p>If a shop purchase or save upload times out, I do not want to guess whether the server processed it already. I want the API to make that answer obvious.</p>
<p>For writes, I usually do one of two things:</p>
<ol>
<li>design the operation so repeating it is harmless, or</li>
<li>attach an idempotency key so the server can deduplicate the request</li>
</ol>
<p>Stripe’s idempotency model is a good example of the second approach: the client provides a key, the server remembers the first result, and retries with the same key get the same answer instead of double-applying the operation. I want that behavior for anything expensive or player-visible.</p>
<p>If I own the API, I make retries explicit instead of hoping the network is kind.</p>
<h2 id="status-codes-are-part-of-the-api">Status codes are part of the API</h2>
<p>One of the worst habits in API code is returning <code>200 OK</code> for everything and putting the real outcome in a JSON field called <code>success</code>.</p>
<p>That throws away the status channel HTTP already gives me.</p>
<p>HTTP already gives me a clean status channel, so I use it.</p>
<p>A few codes do a lot of work for me:</p>
<ul>
<li><code>200 OK</code> — the request succeeded and there is a response body</li>
<li><code>201 Created</code> — a new resource was created</li>
<li><code>204 No Content</code> — the request succeeded but there is nothing to return</li>
<li><code>400 Bad Request</code> — the client sent invalid data</li>
<li><code>401 Unauthorized</code> — the client is not authenticated</li>
<li><code>403 Forbidden</code> — the client is authenticated but not allowed</li>
<li><code>404 Not Found</code> — the resource does not exist</li>
<li><code>409 Conflict</code> — the request collided with the current state</li>
<li><code>429 Too Many Requests</code> — the client is being rate limited</li>
<li><code>500</code> / <code>503</code> — the server failed or is temporarily unavailable</li>
</ul>
<p>I like this because it keeps my client logic honest.</p>
<p>A validation bug is not the same as a network outage. A missing entitlement is not the same as a forbidden action. A conflict is not the same as a generic failure.</p>
<p>If I collapse all of those into one generic error, I make the client dumber and the player experience worse.</p>
<h2 id="timeouts-retries-and-backoff-need-to-be-explicit">Timeouts, retries, and backoff need to be explicit</h2>
<p>If I let the default timeout or retry behavior make decisions for me, I usually regret it later.</p>
<p>Games are sensitive to latency in a way regular business apps often are not. A player will not wait forever for a purchase screen, a cloud save sync, or a matchmaking button that looks frozen.</p>
<p>So I set the rules myself:</p>
<ul>
<li>short, explicit timeouts for player-facing flows</li>
<li>retries only for requests that are safe to repeat</li>
<li>exponential backoff with jitter when I retry</li>
<li>respect <code>Retry-After</code> when the server asks me to slow down</li>
<li>cancellation when the player leaves the screen or the object is destroyed</li>
</ul>
<p>Cancellation is easy to ignore and painful to debug.</p>
<p>If a menu closes, a scene changes, or a network request is no longer relevant, I want the request to stop. Otherwise I end up applying stale data to a UI that no longer exists.</p>
<p>That is a lifecycle problem, but HTTP code tends to expose it first.</p>
<h2 id="keep-auth-boring-and-separate-from-gameplay-logic">Keep auth boring and separate from gameplay logic</h2>
<p>Authentication should be a transport concern, not a gameplay concern.</p>
<p>I do not want random systems reaching into a token cache or building their own auth headers. I want the API layer to own that.</p>
<p>The usual shape is straightforward:</p>
<ul>
<li>access token goes in the <code>Authorization</code> header</li>
<li>refresh token stays in a secure storage path if the platform allows it</li>
<li>gameplay code asks for a service call, not for a raw token</li>
<li>expired tokens fail in one place, not ten</li>
</ul>
<p>And no, I do not put secrets in the URL.</p>
<p>Query strings get logged, cached, copied, and leaked in all the ways people forget about until they are cleaning up an incident. Headers exist for this reason.</p>
<p>If I can make one rule here, it is this: the rest of the game should know whether the player is authenticated, but it should not need to know how the credential is represented.</p>
<h2 id="keep-dtos-away-from-game-state">Keep DTOs away from game state</h2>
<p>This one saves me from a lot of future regret.</p>
<p>The shape of a response from the server is not the same thing as the shape of my gameplay objects.</p>
<p>I keep those apart.</p>
<p>The response object is a DTO: a dumb data packet that mirrors the API contract. The gameplay object is my own model, which can enforce invariants, compute derived values, and survive API changes without pulling the whole codebase apart.</p>
<p>The separation helps because:</p>
<ul>
<li>the API can evolve without rewriting gameplay code</li>
<li>the client can validate inputs before they touch the rest of the game</li>
<li>I can keep server-only fields out of local state</li>
<li>I can mock or replay responses in tests and tools</li>
</ul>
<p>It is tempting to deserialize JSON straight into a live game object because it is one line shorter. That line savings is fake. The cost shows up later when the server changes a field name, adds an optional property, or returns a partial response.</p>
<p>I would rather have one boring mapper than a future bug hunt.</p>
<h2 id="a-small-client-wrapper-is-usually-enough">A small client wrapper is usually enough</h2>
<p>I do not need a giant networking abstraction to get most of the benefit.</p>
<p>I need one place that handles the repetitive stuff:</p>
<ul>
<li>base URL</li>
<li>headers</li>
<li>auth</li>
<li>JSON serialization</li>
<li>status-code mapping</li>
<li>logging</li>
<li>timeouts and cancellation</li>
</ul>
<p>The wrapper can be tiny.</p>
<p>Here is the kind of shape I like in C#-style code:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">struct</span> ApiResponse&lt;T&gt;(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">bool</span> Success,
</span></span><span style="display:flex;"><span>    T? Value,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> StatusCode,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">string?</span> ErrorMessage,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">string?</span> RequestId);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">GameApiClient</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> HttpClient _http;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> JsonSerializerOptions _jsonOptions;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> GameApiClient(HttpClient http)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _http = http;
</span></span><span style="display:flex;"><span>        _jsonOptions = <span style="color:#66d9ef">new</span> JsonSerializerOptions
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            PropertyNameCaseInsensitive = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">async</span> Task&lt;ApiResponse&lt;PlayerProfileDto&gt;&gt; GetProfileAsync(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">string</span> playerId,
</span></span><span style="display:flex;"><span>        CancellationToken token)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">using</span> var request = <span style="color:#66d9ef">new</span> HttpRequestMessage(
</span></span><span style="display:flex;"><span>            HttpMethod.Get,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">$&#34;players/{Uri.EscapeDataString(playerId)}/profile&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        request.Headers.Accept.Add(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">new</span> MediaTypeWithQualityHeaderValue(<span style="color:#e6db74">&#34;application/json&#34;</span>));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">using</span> var response = <span style="color:#66d9ef">await</span> _http.SendAsync(
</span></span><span style="display:flex;"><span>            request,
</span></span><span style="display:flex;"><span>            HttpCompletionOption.ResponseHeadersRead,
</span></span><span style="display:flex;"><span>            token);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> body = <span style="color:#66d9ef">await</span> response.Content.ReadAsStringAsync(token);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> requestId = response.Headers.TryGetValues(<span style="color:#e6db74">&#34;x-request-id&#34;</span>, <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> values)
</span></span><span style="display:flex;"><span>            ? <span style="color:#66d9ef">string</span>.Join(<span style="color:#e6db74">&#34;,&#34;</span>, values)
</span></span><span style="display:flex;"><span>            : <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (!response.IsSuccessStatusCode)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> ApiResponse&lt;PlayerProfileDto&gt;(
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>                (<span style="color:#66d9ef">int</span>)response.StatusCode,
</span></span><span style="display:flex;"><span>                body,
</span></span><span style="display:flex;"><span>                requestId);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> dto = JsonSerializer.Deserialize&lt;PlayerProfileDto&gt;(body, _jsonOptions);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (dto <span style="color:#66d9ef">is</span> <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> ApiResponse&lt;PlayerProfileDto&gt;(
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>                (<span style="color:#66d9ef">int</span>)response.StatusCode,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;Invalid or empty JSON payload.&#34;</span>,
</span></span><span style="display:flex;"><span>                requestId);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> ApiResponse&lt;PlayerProfileDto&gt;(<span style="color:#66d9ef">true</span>, dto, (<span style="color:#66d9ef">int</span>)response.StatusCode, <span style="color:#66d9ef">null</span>, requestId);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The example is deliberately plain.</p>
<p>I care less about the syntax than the result: the wrapper turns a messy network exchange into a consistent result type. Once I have that, the rest of the game can stop asking whether a request failed because of transport, auth, validation, or rate limiting.</p>
<p>In Unity runtime code I would usually wrap <code>UnityWebRequest</code> instead of using <code>HttpClient</code> directly, but the design pressure is the same. I still want one place that owns the HTTP rules.</p>
<h2 id="the-mistakes-i-keep-seeing">The mistakes I keep seeing</h2>
<p>A few mistakes come up over and over again.</p>
<h3 id="1-treating-every-failure-as-a-generic-error">1. Treating every failure as a generic error</h3>
<p>If a request fails, I want to know <strong>why</strong>.</p>
<p>I do not want a one-size-fits-all <code>false</code> result. I want the status code, any server message, and ideally a request ID I can hand to someone with logs.</p>
<h3 id="2-retrying-unsafe-writes-blindly">2. Retrying unsafe writes blindly</h3>
<p>If I repeat a non-idempotent <code>POST</code> after a timeout, I may create duplicate purchases, duplicate inventory grants, or duplicate save entries.</p>
<p>That creates support tickets.</p>
<h3 id="3-putting-business-rules-in-transport-code">3. Putting business rules in transport code</h3>
<p>The networking layer should not decide whether a player is allowed to equip an item. It should move data.</p>
<p>Validation belongs close to the contract, and gameplay rules belong in gameplay code.</p>
<h3 id="4-letting-the-server-and-client-disagree-silently">4. Letting the server and client disagree silently</h3>
<p>If the server changes a field name, adds a required value, or starts returning a different shape, I want that to fail loudly in staging.</p>
<p>Silent fallback is not resilience if it just hides a contract break.</p>
<h3 id="5-ignoring-rate-limits">5. Ignoring rate limits</h3>
<p>If an API returns <code>429</code>, it is telling me to slow down.</p>
<p>The correct response is usually to back off, not to hammer harder.</p>
<h2 id="when-http-is-a-good-fit-and-when-it-is-not">When HTTP is a good fit, and when it is not</h2>
<p>HTTP is a great fit for a lot of game-adjacent work:</p>
<ul>
<li>account services</li>
<li>cloud saves</li>
<li>inventory and cosmetics</li>
<li>live-ops configuration</li>
<li>telemetry and analytics</li>
<li>matchmaking discovery</li>
<li>build pipelines and editor tooling</li>
</ul>
<p>It is less exciting for high-frequency real-time simulation.</p>
<p>If I need to move player movement, hit detection, or frame-by-frame state at low latency, HTTP is usually the wrong tool. That is where I start looking at a persistent transport with lower overhead and better timing guarantees.</p>
<p>So I do not use HTTP for everything. I use it where the request/response model fits the problem.</p>
<p>That is usually a sign I am making a sane choice.</p>
<h2 id="what-i-would-keep-in-my-head-while-designing-an-api">What I would keep in my head while designing an API</h2>
<p>If I had to compress all of this into a short checklist, it would be this:</p>
<ul>
<li>use HTTP methods for their real meaning</li>
<li>return real status codes, not just <code>success = true</code></li>
<li>make retries safe or explicit</li>
<li>respect <code>Retry-After</code> and add backoff</li>
<li>keep auth in the API layer</li>
<li>separate DTOs from gameplay state</li>
<li>log request IDs and useful failures</li>
<li>use one wrapper, not ten one-off request paths</li>
</ul>
<p>This is enough to keep most API work from turning into a maintenance tax.</p>
<p>Sending the request is easy. The hard part is making the request safe to repeat, easy to debug, and easy to evolve without breaking every caller.</p>
<p>If I get those three right, the rest is plumbing.</p>
<h2 id="references">References</h2>
<p>References I leaned on:</p>
<ul>
<li>MDN, <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview">Overview of HTTP</a></li>
<li>IETF RFC 9110, <a href="https://www.rfc-editor.org/rfc/rfc9110">HTTP Semantics</a></li>
<li>Microsoft Learn, <a href="https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines">HttpClient guidelines for .NET</a></li>
<li>Unity Docs, <a href="https://docs.unity3d.com/ScriptReference/Networking.UnityWebRequest.html">UnityWebRequest</a></li>
<li>Stripe Docs, <a href="https://docs.stripe.com/api/idempotent_requests">Idempotent requests</a></li>
</ul>
<h2 id="the-rule-i-keep">The rule I keep</h2>
<p>If I want game code to survive real network conditions, I stop treating HTTP like a fire-and-forget pipe.</p>
<p>I define the contract clearly, make retries safe, preserve status codes, and keep transport concerns out of gameplay logic. The result is API code I can debug today and change later.</p>
]]></content>
        </item>
        
        <item>
            <title>Zero-Allocation LINQ in Unity: Keep the Query Style, Lose the Garbage</title>
            <link>https://blog.jonmikeys.com/posts/zero-allocation-linq-in-unity/</link>
            <pubDate>Mon, 10 Aug 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/zero-allocation-linq-in-unity/</guid>
            <description>A practical look at LINQ in Unity, where the allocations come from, and when a zero-allocation alternative like ZLinq is worth using.</description>
            <content type="html"><![CDATA[<p>I like LINQ.</p>
<p>That is the honest starting point, because the problem is not the syntax. The problem is that LINQ’s nice shape can hide work in places where Unity really cares about work. A query that reads beautifully in a menu, a tool script, or a one-off data pass can become a quiet source of garbage when it runs every frame.</p>
<p>That is where zero-allocation LINQ earns a look. It keeps the query style, but changes the implementation shape so the query chain itself does not keep creating little heap objects along the way. ZLinq is one concrete example, and I reach for that idea only when the profiler says the code path is worth caring about.</p>
<p>The rule is still simple: keep the readability when it costs almost nothing, and stop paying for it when the loop is hot.</p>
<p><img src="/images/posts/zero-allocation-linq-in-unity/diagram.svg" alt="Zero Allocation Linq In Unity diagram"></p>
<h2 id="the-real-problem-is-not-linq-it-is-using-linq-in-the-wrong-place">The real problem is not LINQ, it is using LINQ in the wrong place</h2>
<p>LINQ is not automatically bad for games.</p>
<p>I use it all the time in places that are not performance critical:</p>
<ul>
<li>editor tools</li>
<li>startup data shaping</li>
<li>config loading</li>
<li>tests</li>
<li>one-off maintenance code</li>
</ul>
<p>Those are the easy wins. The code is short, the intent is obvious, and the cost is usually irrelevant.</p>
<p>The trouble starts when LINQ sneaks into a path that runs a lot:</p>
<ul>
<li><code>Update</code></li>
<li><code>LateUpdate</code></li>
<li><code>FixedUpdate</code></li>
<li>AI ticks</li>
<li>crowd queries</li>
<li>UI refresh loops</li>
<li>path evaluation</li>
<li>projectile filtering</li>
</ul>
<p>At that point, the same pleasant syntax can become a recurring cost. Unity’s managed-memory guidance is blunt about the bigger picture: automatic memory management is convenient, but garbage collection has a performance cost, so managed allocations need to be treated carefully in real-time code. That is the part people skip when they are moving fast.</p>
<p>I do not object to LINQ because it is elegant. I object to using elegant code as a disguise for repeated allocation.</p>
<h2 id="where-the-cost-usually-comes-from">Where the cost usually comes from</h2>
<p>A LINQ chain is not just a fancy <code>for</code> loop.</p>
<p>It is usually a pipeline of iterator objects, delegates, and deferred work. Some of that can be cheap in practice, but it is still work. The common allocation traps are usually some mix of these:</p>
<ul>
<li>iterator objects for the query chain</li>
<li>closures when a lambda captures local state</li>
<li>boxing when values get pushed through the wrong abstraction</li>
<li>repeated enumeration when a query is walked more than once</li>
<li>temporary lists or arrays created to force a query to materialize</li>
</ul>
<p>That matters because game code rarely runs one query once. It runs many queries, every frame, over and over again.</p>
<p>A simple example looks harmless enough:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> visibleEnemies = enemies
</span></span><span style="display:flex;"><span>    .Where(e =&gt; e.IsAlive &amp;&amp; e.DistanceTo(player) &lt; aggroRange)
</span></span><span style="display:flex;"><span>    .Select(e =&gt; e.Transform);
</span></span></code></pre></div><p>That reads well. It also hides a few questions that matter in a hot path:</p>
<ul>
<li>Is <code>enemies</code> a concrete list, or just an <code>IEnumerable&lt;T&gt;</code>?</li>
<li>Does the lambda capture <code>aggroRange</code> or other local state?</li>
<li>Is this query enumerated once, or several times?</li>
<li>Does the result need to live beyond this frame?</li>
</ul>
<p>The query is still fine if the answer to those questions is “this is rare.” It is less fine if the answer is “every enemy tick, every frame.”</p>
<h2 id="deferred-execution-is-useful-but-it-can-surprise-you">Deferred execution is useful, but it can surprise you</h2>
<p>Deferred execution is one of the best things about LINQ, and also one of the easiest things to misread.</p>
<p>The query does not usually do the work when you write it. It does the work when you enumerate it. Microsoft’s LINQ documentation calls this out directly: evaluation is delayed until the value is actually required.</p>
<p>That is useful because it lets you build pipelines cheaply and only pay when you need the result. It is also dangerous because the query is not a frozen snapshot unless you make it one.</p>
<p>I keep three rules in mind:</p>
<ol>
<li>
<p><strong>If I need a stable result, materialize it.</strong>
Use <code>ToList</code>, <code>ToArray</code>, or whatever the project already standardizes on.</p>
</li>
<li>
<p><strong>If I need to walk the result twice, do not rely on a deferred query.</strong>
Enumerating twice usually means doing the work twice.</p>
</li>
<li>
<p><strong>If the source can change underneath me, be explicit about when the query should observe that change.</strong>
Lazy evaluation is a feature only when the timing is deliberate.</p>
</li>
</ol>
<p>That is not a LINQ problem. That is a “I forgot the query is a pipeline, not a snapshot” problem.</p>
<h2 id="the-shape-i-use-before-reaching-for-a-zero-allocation-library">The shape I use before reaching for a zero-allocation library</h2>
<p>The first thing I do is not install a package.</p>
<p>I look at the code and ask whether a plain loop is the cleanest answer. Sometimes it is. A direct loop is still the least surprising thing in the room, and in a hot path it is often the easiest way to be explicit about allocations, branching, and early exits.</p>
<p>For example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>Transform? nearest = <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">float</span> bestDistance = <span style="color:#66d9ef">float</span>.MaxValue;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i = <span style="color:#ae81ff">0</span>; i &lt; enemies.Count; i++)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> enemy = enemies[i];
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (!enemy.IsAlive)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">continue</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> distance = Vector3.Distance(enemy.Position, player.Position);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (distance &gt;= aggroRange || distance &gt;= bestDistance)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">continue</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    bestDistance = distance;
</span></span><span style="display:flex;"><span>    nearest = enemy.Transform;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That code is not glamorous, but it is honest.</p>
<p>It tells me exactly when the work stops. It does not hide a lambda capture. It does not create a query object chain. It does not pretend the code is cheaper than it is.</p>
<p>If the loop is short and the logic is local, I usually stop there.</p>
<h2 id="when-i-want-query-style-without-the-allocation-shape">When I want query style without the allocation shape</h2>
<p>Sometimes I still want the query style.</p>
<p>That is the niche for a zero-allocation LINQ library like ZLinq. The idea is simple: keep the familiar <code>Where</code>, <code>Select</code>, and related operators, but implement the chain with value types instead of heap-backed iterator objects. ZLinq’s README describes it as zero-allocation LINQ, with <code>AsValueEnumerable()</code> turning existing collections into a chainable value enumerable.</p>
<p>That is the pitch I care about in Unity:</p>
<ul>
<li>keep the readable query shape</li>
<li>reduce allocations in the chain</li>
<li>stay closer to the way the code already reads</li>
</ul>
<p>A typical use looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> ZLinq;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> targets = enemies
</span></span><span style="display:flex;"><span>    .AsValueEnumerable()
</span></span><span style="display:flex;"><span>    .Where(e =&gt; e.IsAlive)
</span></span><span style="display:flex;"><span>    .Where(e =&gt; e.DistanceTo(player) &lt; aggroRange)
</span></span><span style="display:flex;"><span>    .Select(e =&gt; e.Transform);
</span></span></code></pre></div><p>If the project is already comfortable with query syntax, that can be a nice middle ground. I do not have to turn every hot query into a manual loop just to keep GC quiet.</p>
<p>That matters because readability is not fake value. If a low-level rewrite makes the code harder to reason about, the trade was not free.</p>
<h2 id="zlinq-is-not-a-license-to-ignore-the-profiler">ZLinq is not a license to ignore the profiler</h2>
<p>I still measure.</p>
<p>That is the part that saves me from premature optimization. A lot of code is “hot” in theory and harmless in practice. A lot of code looks cheap and turns out to be a real problem once the scene gets bigger.</p>
<p>In Unity, the useful checks are still the boring ones:</p>
<ul>
<li>profile the actual player build, not just the Editor</li>
<li>look at <code>GC.Alloc</code></li>
<li>check the frame where the code runs, not just a microbenchmark</li>
<li>compare the before and after case under the same conditions</li>
</ul>
<p>Unity’s guidance on managed memory and garbage collection makes the same point: the cost is in the runtime behavior, so the runtime behavior is what you need to inspect.</p>
<p>That is why I do not treat zero-allocation LINQ as a default. I treat it as a targeted fix for places where the profiler proves that the standard LINQ shape is too expensive.</p>
<h2 id="the-tradeoffs-are-real">The tradeoffs are real</h2>
<p>There is no free lunch here.</p>
<p>Zero-allocation LINQ usually improves one thing while making something else a little less convenient.</p>
<p>The tradeoffs I actually care about are these:</p>
<h3 id="1-familiarity">1. Familiarity</h3>
<p>Standard LINQ is everywhere in C# code. ZLinq’s API is intentionally close, but it is still another library and another mental model. That means team members have to know when they are looking at a normal enumerable and when they are looking at a value enumerable.</p>
<h3 id="2-type-inference-and-signatures">2. Type inference and signatures</h3>
<p>Value-type query chains can be a little less forgiving when code gets clever. If a chain starts relying on complex generic inference, the nice syntax can stop being nice.</p>
<h3 id="3-ecosystem-compatibility">3. Ecosystem compatibility</h3>
<p>A lot of C# APIs expect <code>IEnumerable&lt;T&gt;</code>. A value enumerable is great when the whole chain stays inside the library, but sometimes I need to cross back into the normal world. At that point, a materialization step or a regular enumerable adapter may be the right boundary.</p>
<h3 id="4-project-complexity">4. Project complexity</h3>
<p>Every dependency is another thing to keep alive. If a query only runs a few times per session, I do not want to introduce a specialized library just to avoid a cost that does not matter.</p>
<p>That is the real balance: use the simpler thing until the simpler thing becomes expensive.</p>
<h2 id="the-shape-of-a-good-rule">The shape of a good rule</h2>
<p>When I am deciding between plain LINQ, a loop, and a zero-allocation alternative, I use a small set of questions.</p>
<h3 id="use-plain-linq-when">Use plain LINQ when:</h3>
<ul>
<li>the code is not on a hot path</li>
<li>clarity matters more than micro-cost</li>
<li>the query runs rarely</li>
<li>the team already understands the shape</li>
</ul>
<h3 id="use-a-manual-loop-when">Use a manual loop when:</h3>
<ul>
<li>the path is hot and simple</li>
<li>I need precise control over branching or early exits</li>
<li>I want the most obvious allocation story possible</li>
<li>the query is only doing one small job</li>
</ul>
<h3 id="use-zero-allocation-linq-when">Use zero-allocation LINQ when:</h3>
<ul>
<li>the query is hot enough to care about</li>
<li>I still want query-style readability</li>
<li>the team is okay with the extra library and its constraints</li>
<li>profiling showed the LINQ shape was part of the problem</li>
</ul>
<p>That is the whole decision tree. I do not need a bigger one.</p>
<h2 id="a-practical-example-selecting-active-targets-every-frame">A practical example: selecting active targets every frame</h2>
<p>Suppose I have a combat system that needs a filtered list of targets for a short-lived attack pass.</p>
<p>The naive version might build a query every frame and then walk it several times:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> activeTargets = allTargets
</span></span><span style="display:flex;"><span>    .Where(t =&gt; t.IsAlive)
</span></span><span style="display:flex;"><span>    .Where(t =&gt; t.IsVisible)
</span></span><span style="display:flex;"><span>    .Where(t =&gt; t.DistanceTo(player) &lt; attackRange);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (activeTargets.Any())
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">foreach</span> (<span style="color:#66d9ef">var</span> target <span style="color:#66d9ef">in</span> activeTargets)
</span></span><span style="display:flex;"><span>        target.Highlight();
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is readable, but I immediately have a few questions:</p>
<ul>
<li>am I enumerating the same query more than once?</li>
<li>do I really need deferred execution here?</li>
<li>is the source collection stable enough to depend on that laziness?</li>
<li>am I paying for clarity in a place where I should be paying for speed?</li>
</ul>
<p>A better shape is often one of these two:</p>
<p><strong>Option A: materialize once</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> activeTargets = allTargets
</span></span><span style="display:flex;"><span>    .Where(t =&gt; t.IsAlive &amp;&amp; t.IsVisible &amp;&amp; t.DistanceTo(player) &lt; attackRange)
</span></span><span style="display:flex;"><span>    .ToArray();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (activeTargets.Length &gt; <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i = <span style="color:#ae81ff">0</span>; i &lt; activeTargets.Length; i++)
</span></span><span style="display:flex;"><span>        activeTargets[i].Highlight();
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Option B: stay in query style with a zero-allocation chain</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> ZLinq;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> activeTargets = allTargets
</span></span><span style="display:flex;"><span>    .AsValueEnumerable()
</span></span><span style="display:flex;"><span>    .Where(t =&gt; t.IsAlive)
</span></span><span style="display:flex;"><span>    .Where(t =&gt; t.IsVisible)
</span></span><span style="display:flex;"><span>    .Where(t =&gt; t.DistanceTo(player) &lt; attackRange);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">foreach</span> (<span style="color:#66d9ef">var</span> target <span style="color:#66d9ef">in</span> activeTargets)
</span></span><span style="display:flex;"><span>    target.Highlight();
</span></span></code></pre></div><p>Which one I pick depends on the actual use case.</p>
<p>If I need a stable snapshot, I materialize it. If I just need to stream through the results once and I care about the query cost, the value-enumerable version is attractive. If the code is simple enough, I still might choose the loop and stop thinking about it.</p>
<p>The point is not to worship one shape.</p>
<p>The point is to stop paying for the wrong shape in the wrong place.</p>
<h2 id="what-usually-goes-wrong-when-people-try-to-optimize-this">What usually goes wrong when people try to optimize this</h2>
<p>The biggest mistake is to turn every LINQ call into a crusade.</p>
<p>I have seen codebases where someone replaced every query with hand-written loops long before the profiler proved it was needed. That usually makes the code worse. It removes readability from cold code and adds maintenance cost everywhere.</p>
<p>I have also seen the opposite mistake: people keep a chain of LINQ calls in a hot loop because the code is pretty and they do not want to touch it. That is how a small allocation becomes a frame-time tax.</p>
<p>Both mistakes come from skipping the same step: measure first.</p>
<p>If a query shows up in the profiler, I fix it.</p>
<p>If it does not, I leave it alone.</p>
<p>That rule saves a lot of time.</p>
<h2 id="one-more-thing-about-closures">One more thing about closures</h2>
<p>Closures are the silent tax people forget to look for.</p>
<p>When a lambda captures local state, the compiler may need to create a hidden object to hold that state. In a one-off query, that is not a big deal. In a per-frame query, it can be the difference between a clean line and a slow one.</p>
<p>This is one reason I like explicit code in hot paths. A captured variable is easy to miss during review because the lambda looks small. The allocation is invisible unless I am watching for it.</p>
<p>If I need a hot query and I want to avoid that trap, I either:</p>
<ul>
<li>write the loop directly</li>
<li>push the needed state into a structure I control</li>
<li>use a value-enumerable approach where the library is designed around that cost model</li>
</ul>
<p>The main point is not the specific syntax. The point is to stop pretending closures are free.</p>
<h2 id="my-practical-takeaway">My practical takeaway</h2>
<p>I do not think of zero-allocation LINQ as a replacement for normal LINQ.</p>
<p>I think of it as a pressure valve.</p>
<p>Standard LINQ is still great when the code is clear, local, and not performance critical. A direct loop is still the cleanest answer when I want absolute control. ZLinq sits in the middle when I want query style without the usual allocation shape.</p>
<p>That is the useful mental model:</p>
<ul>
<li>prefer normal LINQ for clarity</li>
<li>prefer loops for the simplest hot paths</li>
<li>use zero-allocation LINQ when the profiler says the query style is worth keeping, but the garbage is not</li>
</ul>
<p>If I stay disciplined about that split, I keep the nice code where it belongs and the fast code where it matters.</p>
<h2 id="references">References</h2>
<ul>
<li>Cysharp, <a href="https://github.com/Cysharp/ZLinq">ZLinq README</a></li>
<li>Unity Manual, <a href="https://docs.unity3d.com/Manual/performance-optimizing-code-managed-memory.html">Optimizing managed memory</a></li>
<li>Microsoft Learn, <a href="https://learn.microsoft.com/en-us/dotnet/standard/linq/deferred-execution-lazy-evaluation">Deferred execution and lazy evaluation</a></li>
<li>Microsoft Learn, <a href="https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.where">Enumerable.Where Method</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Native Collections and Burst for GC-Heavy Projects</title>
            <link>https://blog.jonmikeys.com/posts/native-collections-and-burst-for-gc-heavy-projects/</link>
            <pubDate>Thu, 06 Aug 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/native-collections-and-burst-for-gc-heavy-projects/</guid>
            <description>A practical way to move hot data off the managed heap with NativeArray, NativeList, jobs, and Burst without turning the project into DOTS cosplay.</description>
            <content type="html"><![CDATA[<p>If a Unity project is spending too much time in garbage collection, I do not start by looking for a silver bullet.</p>
<p>I start by asking a much duller question: which data path keeps making the managed heap work harder than it needs to?</p>
<p>That is usually where the real win lives. Not in a heroic rewrite. Not in a new architecture. Just in moving the right hot data out of the managed heap, into a native container, and into code that Burst can compile into something the CPU actually likes.</p>
<p>That combination is not a cure for bad design. It is a way to stop paying garbage-collector tax on code that runs every frame.</p>
<p><img src="/images/posts/native-collections-and-burst-for-gc-heavy-projects/diagram.svg" alt="Managed data to native data flow diagram"></p>
<h2 id="what-problem-i-am-actually-trying-to-solve">What problem I am actually trying to solve</h2>
<p>A lot of Unity performance advice gets flattened into a single word: &ldquo;GC&rdquo;.</p>
<p>That is too vague to be useful.</p>
<p>What I care about is managed allocation pressure. If a hot path keeps creating short-lived objects, arrays, closures, or temporary collections, the runtime has to clean them up later. The cleanup is not free, and the cost usually shows up at the worst possible moment: a frame hitch, a spike, or a stall that only appears when the scene gets busy.</p>
<p>Unity’s own memory documentation says the managed memory system is convenient, but the garbage collector is unpredictable enough to cause performance problems. The profiling docs also make the obvious but important point that memory use has to be measured, not guessed.</p>
<p>So my first move is not &ldquo;switch everything to native memory&rdquo;. My first move is &ldquo;find the hot path that keeps allocating and decide whether the data shape is simple enough to move&rdquo;.</p>
<p>That distinction matters.</p>
<p>If the code is cold, managed memory is fine. If the code runs once on load, or once per match, or once per menu transition, I do not need to drag native containers into it just to feel clever. If the code runs every frame, on thousands of entities, or in a tight simulation loop, then I start paying attention.</p>
<h2 id="what-native-containers-buy-me">What native containers buy me</h2>
<p>Unity’s NativeContainer types are thread-safe wrappers around native memory. That is the short version, and it is the one I keep in my head.</p>
<p><code>NativeArray&lt;T&gt;</code> is the familiar one. <code>NativeSlice&lt;T&gt;</code> gives me a view into part of an array. The Collections package adds more options like <code>NativeList</code>, <code>NativeHashMap</code>, and friends, which are useful when the data is still dynamic but I do not want it sitting on the managed heap.</p>
<p>The important part is not the type name. It is the memory model.</p>
<p>Native containers let me keep data outside managed memory, share it with jobs, and control the allocation lifetime explicitly. That gives me three practical wins:</p>
<ul>
<li>less managed allocation pressure</li>
<li>data that jobs can read and write safely</li>
<li>a clearer lifetime story than &ldquo;hope the GC gets around to it&rdquo;</li>
</ul>
<p>The price is that I have to be honest about ownership. Native memory does not magically disappear when a scope ends. I have to allocate it with the right allocator and dispose of it myself.</p>
<p>That is not a bug. That is the trade.</p>
<h2 id="the-diagram-i-keep-in-mind">The diagram I keep in mind</h2>
<p>I like a very simple mental model for this.</p>
<p>The managed side owns the game’s ordinary objects, UI scripts, and higher-level orchestration.
The native side owns the hot data that needs to move quickly.
Burst takes the pure data transform and makes it cheaper to run.</p>
<p>Once I frame it that way, the decision becomes easier:</p>
<ul>
<li>if the code needs object graphs, inheritance, or engine APIs, keep it managed</li>
<li>if the code is mostly numbers, flags, positions, counts, or results, consider native containers</li>
<li>if the work is embarrassingly parallel and data-driven, consider a Burst job</li>
</ul>
<p>That is usually the whole conversation.</p>
<h2 id="allocators-are-the-part-people-skip-and-regret-later">Allocators are the part people skip and regret later</h2>
<p>Native containers do not work unless I choose the allocator deliberately.</p>
<p>Unity’s NativeContainer docs are explicit about the common options:</p>
<ul>
<li><code>Allocator.Temp</code> is the fastest and is meant for one frame or less</li>
<li><code>Allocator.TempJob</code> is for short-lived job data and has a four-frame lifetime expectation</li>
<li><code>Allocator.Persistent</code> lasts as long as I need it to, but it is the slowest of the three</li>
</ul>
<p>That is the part that turns native memory from a performance tool into an actual responsibility.</p>
<p>I use the allocator as a clue about intent:</p>
<ul>
<li><strong>Temp</strong> means scratch work I will not keep</li>
<li><strong>TempJob</strong> means a short-lived buffer around a job or a small pipeline step</li>
<li><strong>Persistent</strong> means a buffer I intend to reuse across frames or scenes</li>
</ul>
<p>For most gameplay code, <code>TempJob</code> is the right first try when the data must survive long enough for a job to finish. If I already know I will use the same buffer every frame, I stop allocating it every frame and make it persistent instead.</p>
<p>That last step is where a lot of the benefit comes from. Reuse beats churn.</p>
<h2 id="burst-is-not-the-magic-it-is-the-amplifier">Burst is not the magic; it is the amplifier</h2>
<p>Burst is an ahead-of-time compiler for a subset of C#.</p>
<p>Unity’s Burst docs describe it plainly: it compiles compatible code into highly optimized native CPU code. It was designed for jobs, but it can also compile static methods when the code fits the supported subset. Unity’s blog posts on Burst call out the same theme: Burst is most valuable when the work is CPU-bound and broken into data-oriented pieces that can exploit parallelism.</p>
<p>That is the key sentence.</p>
<p>Burst is not a license to write any old C# and hope for miracles. It rewards code that is already simple, data-driven, and predictable. If the data is scattered across objects, or if the code keeps reaching back into UnityEngine APIs, Burst will not rescue the shape of the problem.</p>
<p>So I treat Burst as a force multiplier, not a substitute for structure.</p>
<p>If I can turn a loop over positions, velocities, visibility flags, heat values, or spawn rules into a pure data transform, Burst is worth looking at. If the code is mostly orchestration, scene plumbing, or UI control flow, Burst is usually the wrong tool.</p>
<h2 id="a-small-job-is-the-easiest-honest-example">A small job is the easiest honest example</h2>
<p>Suppose I want to cull points against a radius. That is simple enough to show the shape without dragging in a giant subsystem.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> Unity.Burst;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> Unity.Collections;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> Unity.Jobs;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> Unity.Mathematics;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">[BurstCompile]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">DistanceCullJob</span> : IJobParallelFor
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [ReadOnly]</span> <span style="color:#66d9ef">public</span> NativeArray&lt;float3&gt; Positions;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [WriteOnly]</span> <span style="color:#66d9ef">public</span> NativeArray&lt;<span style="color:#66d9ef">byte</span>&gt; Visible;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> float3 Center;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> RadiusSq;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Execute(<span style="color:#66d9ef">int</span> index)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> delta = Positions[index] - Center;
</span></span><span style="display:flex;"><span>        Visible[index] = (<span style="color:#66d9ef">byte</span>)(math.lengthsq(delta) &lt;= RadiusSq ? <span style="color:#ae81ff">1</span> : <span style="color:#ae81ff">0</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That job does one thing. It reads positions. It writes visibility results. It does not talk to the scene. It does not allocate. It does not poke at a <code>Transform</code>. It is the kind of code Burst can help.</p>
<p>The main-thread setup is just as important as the job itself:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> positions = <span style="color:#66d9ef">new</span> NativeArray&lt;float3&gt;(count, Allocator.TempJob);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> visible = <span style="color:#66d9ef">new</span> NativeArray&lt;<span style="color:#66d9ef">byte</span>&gt;(count, Allocator.TempJob);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i = <span style="color:#ae81ff">0</span>; i &lt; count; i++)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    positions[i] = points[i];
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> job = <span style="color:#66d9ef">new</span> DistanceCullJob
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    Positions = positions,
</span></span><span style="display:flex;"><span>    Visible = visible,
</span></span><span style="display:flex;"><span>    Center = center,
</span></span><span style="display:flex;"><span>    RadiusSq = radius * radius,
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Ceiling: this batch size is a starting point, not a law.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// If this becomes a real bottleneck, measure and tune it instead of guessing.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> handle = job.Schedule(positions.Length, <span style="color:#ae81ff">64</span>);
</span></span><span style="display:flex;"><span>handle.Complete();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i = <span style="color:#ae81ff">0</span>; i &lt; count; i++)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (visible[i] != <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// use the result</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>visible.Dispose();
</span></span><span style="display:flex;"><span>positions.Dispose();
</span></span></code></pre></div><p>That example is intentionally plain.</p>
<p>The point is not the exact algorithm. The point is the flow:</p>
<ol>
<li>move the data into native memory</li>
<li>run the work as a job</li>
<li>let Burst compile the hot loop</li>
<li>read the results back on the main thread</li>
<li>dispose the containers when finished</li>
</ol>
<p>That is the pattern I reach for when I want a boring, reliable speedup instead of a fragile rewrite.</p>
<h2 id="read-only-data-can-scale-better-than-i-expected">Read-only data can scale better than I expected</h2>
<p>One detail that matters more than people think is the <code>ReadOnly</code> attribute.</p>
<p>Unity’s NativeContainer docs point out that jobs normally get read and write access, but if a job only needs to read data, marking the container <code>[ReadOnly]</code> lets the scheduler run more things in parallel.</p>
<p>That sounds small. It is not.</p>
<p>Read-only data is easier for the job system to schedule safely. It reduces artificial contention. It lets multiple systems consume the same source data without turning one write lock into a traffic jam.</p>
<p>I use that rule aggressively:</p>
<ul>
<li>read-only input buffers stay read-only</li>
<li>output buffers are write-only when possible</li>
<li>jobs should mutate as little shared state as possible</li>
</ul>
<p>If I feel tempted to make everything writable &ldquo;just in case&rdquo;, I treat that as a design smell. Writable data creates scheduling friction.</p>
<h2 id="native-containers-do-not-mean-put-everything-in-native-memory">Native containers do not mean &ldquo;put everything in native memory&rdquo;</h2>
<p>This is where the idea gets misused.</p>
<p>Native containers are not a universal replacement for lists, objects, or scene references. They are a tool for a specific problem shape.</p>
<p>I do not reach for them when:</p>
<ul>
<li>the data is tiny and cold</li>
<li>the code needs rich object behavior</li>
<li>the feature is mostly orchestration</li>
<li>the bottleneck is elsewhere, like rendering or asset loading</li>
<li>the code would become harder to understand than the performance win is worth</li>
</ul>
<p>If a managed <code>List&lt;T&gt;</code> is simple, clear, and fast enough, I leave it alone.</p>
<p>That is not me being timid. That is me avoiding extra lifetime management for no real gain.</p>
<p>The wrong move is to replace every list with a native container because native sounds lower-level. The right move is to convert the specific hot path that keeps generating garbage or stalling the frame.</p>
<h2 id="the-memory-profiler-is-where-i-check-myself">The memory profiler is where I check myself</h2>
<p>I do not trust intuition on this stuff.</p>
<p>Unity’s Memory Profiler module shows where memory is being allocated and how much the runtime is using and reserving. That is useful because a lot of GC problems are misdiagnosed from the outside. A path can feel &ldquo;slow&rdquo; when the real issue is allocation churn. Another path can feel &ldquo;fine&rdquo; until a later frame pays for all the earlier convenience.</p>
<p>So I look for three things:</p>
<ul>
<li>managed allocations that happen repeatedly</li>
<li>native allocations that are never reused</li>
<li>frame spikes that disappear when the hot path stops allocating</li>
</ul>
<p>If I move a hot loop to native memory and the hitch remains, then the problem was never the garbage collector in the first place. Maybe the algorithm is bad. Maybe the data layout is bad. Maybe the work belongs somewhere else.</p>
<p>That is useful too. It means I stopped guessing.</p>
<h2 id="the-mistake-i-see-most-often">The mistake I see most often</h2>
<p>The most common mistake is not using native memory. It is using it without a lifetime plan.</p>
<p>A container gets allocated, passed around, and forgotten. Or it gets reallocated every frame because nobody bothered to keep a persistent buffer around. Or it gets disposed too early because the code path was written as if the job completed instantly.</p>
<p>That is how a performance improvement turns into a crash or a leak.</p>
<p>So I keep a few rules in my head:</p>
<ul>
<li>allocate as late as possible</li>
<li>dispose as early as possible</li>
<li>reuse persistent buffers when the pattern repeats</li>
<li>keep job data plain and serializable in spirit, even if it is not literally serialized</li>
<li>keep engine interaction on the main thread</li>
</ul>
<p>The last rule is especially important. Jobs are for data processing, not for pretending the Unity API became thread-safe overnight.</p>
<h2 id="when-i-would-use-this-in-a-real-game">When I would use this in a real game</h2>
<p>I reach for NativeContainer plus Burst when I see a problem like one of these:</p>
<ul>
<li>thousands of boids, agents, or projectiles need the same math every frame</li>
<li>a visibility, distance, or influence pass is running hot</li>
<li>I am building intermediate arrays for spawn, cull, or sort steps</li>
<li>AI, combat, or simulation data can be flattened into plain structs</li>
<li>the code is CPU-bound and not tied to scene objects</li>
</ul>
<p>That last point matters most.</p>
<p>If the expensive part is mostly computation, I can often move it to jobs and Burst. If the expensive part is scene traversal, asset loading, rendering, or API calls, native memory alone will not help much.</p>
<p>So I use the profiler first, not because that sounds disciplined, but because it saves me from solving the wrong problem.</p>
<h2 id="the-practical-split-that-keeps-me-sane">The practical split that keeps me sane</h2>
<p>When I am deciding whether to convert a path, I ask four questions:</p>
<ol>
<li>Is this code actually hot?</li>
<li>Is the data simple enough to flatten?</li>
<li>Can the work run without touching UnityEngine objects?</li>
<li>Will the memory lifetime be obvious after the change?</li>
</ol>
<p>If the answer to all four is yes, I start with a small native buffer and a small Burst job.</p>
<p>If the answer to one of them is no, I do not force it.</p>
<p>That restraint is the real trick. Native containers and Burst are powerful, but they are easiest to use when the problem is already shaped for them. The more I try to bend a messy object graph into a job shape, the more I fight the tool.</p>
<h2 id="a-good-rule-of-thumb">A good rule of thumb</h2>
<p>If I can describe the work as &ldquo;take this flat data, transform it, and write out results&rdquo;, I am probably in the right neighborhood.</p>
<p>If I need object identity, polymorphism, live scene references, or lots of branching around engine state, I am probably not.</p>
<p>That is a useful line because it keeps me from overengineering. I do not need to migrate an entire system to DOTS just because one loop allocates too much. I only need to move the hot piece of the loop off the managed heap, stop allocating temporary garbage, and let Burst do the simple math faster.</p>
<p>That is enough to make a real difference.</p>
<h2 id="takeaway">Takeaway</h2>
<p>My default approach is simple:</p>
<ul>
<li>profile first</li>
<li>find the allocation-heavy hot path</li>
<li>flatten the data if the shape allows it</li>
<li>use <code>NativeArray</code>, <code>NativeList</code>, or another native container with the right allocator</li>
<li>mark read-only inputs as <code>[ReadOnly]</code></li>
<li>keep the job pure data and let Burst compile the hot loop</li>
<li>reuse persistent buffers instead of churning memory every frame</li>
</ul>
<p>That is not glamorous, but it is the kind of change that actually survives a production project.</p>
<p>If I get the shape right, the GC gets quieter, the frame spikes get smaller, and the code is still understandable six months later. That is the whole point.</p>
<h2 id="references">References</h2>
<ul>
<li>Unity Manual: <a href="https://docs.unity3d.com/Manual/JobSystemNativeContainer.html">Introduction to NativeContainer</a></li>
<li>Unity Scripting API: <a href="https://docs.unity3d.com/ScriptReference/Unity.Collections.NativeArray_1.html">NativeArray<!-- raw HTML omitted --></a></li>
<li>Unity Manual: <a href="https://docs.unity3d.com/Manual/UnderstandingAutomaticMemoryManagement.html">Memory in Unity introduction</a></li>
<li>Unity Manual: <a href="https://docs.unity3d.com/Manual/performance-garbage-collection-best-practices.html">Optimizing your code for managed memory</a></li>
<li>Unity Manual: <a href="https://docs.unity3d.com/Manual/ProfilerMemory.html">Memory Profiler module reference</a></li>
<li>Unity Burst docs: <a href="https://docs.unity3d.com/Packages/com.unity.burst@1.8/manual/index.html">Burst compiler 1.8.30</a></li>
<li>Unity blog: <a href="https://unity.com/blog/engine-platform/enhancing-mobile-performance-with-the-burst-compiler">Enhancing mobile performance with the Burst compiler</a></li>
<li>Unity blog: <a href="https://unity.com/blog/engine-platform/raising-your-game-with-burst-1-7">Raising your game with Burst 1.7</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Event Buses in Unity Architecture: Keep Systems Talking Without Tight Coupling</title>
            <link>https://blog.jonmikeys.com/posts/event-buses-in-unity-architecture/</link>
            <pubDate>Sun, 02 Aug 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/event-buses-in-unity-architecture/</guid>
            <description>A practical way to use event buses in Unity without turning them into hidden global state, leaky subscriptions, or another way to avoid design decisions.</description>
            <content type="html"><![CDATA[<p>I keep coming back to event buses for one simple reason: game code often needs to say &ldquo;something happened&rdquo; without dragging half the project into the conversation.</p>
<p>A player dies. A quest advances. The HUD flashes. A sound plays. Analytics records a milestone. None of those systems needs to know the others exist. They just need a clean way to react to the same moment.</p>
<p>That is where an event bus earns its keep.</p>
<p>It is not magic. It is not a replacement for all references. It is just a transport layer for notifications. Used well, it keeps gameplay code readable. Used badly, it becomes a global junk drawer with a nicer name.</p>
<h2 id="what-i-mean-by-an-event-bus">What I mean by an event bus</h2>
<p>An event bus sits between the thing that raises a message and the things that care about it.</p>
<p>The sender publishes an event.
The bus delivers it to subscribers.
The sender does not need direct references to the listeners.</p>
<p>That is the whole shape.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>player, enemy, UI, system
</span></span><span style="display:flex;"><span>          │
</span></span><span style="display:flex;"><span>          ▼
</span></span><span style="display:flex;"><span>      event bus
</span></span><span style="display:flex;"><span>          │
</span></span><span style="display:flex;"><span>   ┌──────┼────────┬────────┐
</span></span><span style="display:flex;"><span>   ▼      ▼        ▼        ▼
</span></span><span style="display:flex;"><span>hud    audio     quest    save system
</span></span></code></pre></div><p>I like this pattern when the relationship is one-to-many and the sender should stay ignorant of the receivers.</p>
<p>That last part matters. An event bus is not a place to store state. It is not a service locator with better branding. It is not a substitute for a proper ownership model.</p>
<p>If I want to ask &ldquo;what is the current score?&rdquo; I should read score state.
If I want to announce &ldquo;the score changed,&rdquo; I can publish an event.</p>
<p>That distinction keeps the architecture honest.</p>
<h2 id="why-direct-calls-stop-scaling">Why direct calls stop scaling</h2>
<p>Direct references are fine when two objects are obviously related.</p>
<p>A door listens to a trigger.
A health bar watches a health component.
A small feature with two or three moving parts can be clearer with direct calls than with any event system.</p>
<p>The trouble starts when the same style spreads everywhere.</p>
<p>Then I get code that looks like this:</p>
<ul>
<li>enemy death calls score code</li>
<li>score code calls UI code</li>
<li>UI code calls audio code</li>
<li>audio code calls achievement code</li>
<li>achievement code calls analytics code</li>
</ul>
<p>Each link looks harmless on its own. Together they form a chain of invisible assumptions.</p>
<p>The project still works, but every change starts to feel expensive. If I rename a method or split a subsystem, I have to hunt through a pile of unrelated callers. If I load a different scene, some object still thinks the old one should be there. If I want to test one piece in isolation, I need to fake half the game.</p>
<p>That is the real cost.</p>
<p>An event bus helps because it turns those hard references into contracts around meaning.</p>
<p>Not &ldquo;call this object.&rdquo;</p>
<p>&ldquo;When this happens, anyone who cares may react.&rdquo;</p>
<p>That is a much better boundary for cross-cutting game events.</p>
<h2 id="the-diagram-i-keep-in-mind">The diagram I keep in mind</h2>
<p>This is the mental model I use when I decide whether to reach for a bus.</p>
<p><img src="/images/posts/event-buses-in-unity-architecture/diagram.svg" alt="Event bus flow diagram"></p>
<p>The sender only knows about the bus.
The bus knows about subscribers.
The subscribers only know the message type they care about.</p>
<p>That fan-out is the point.</p>
<p>If I need a single response from a specific object, a bus is probably the wrong tool. If I need a broadcast that should reach several systems without forcing them to know about each other, the bus starts to look right.</p>
<h2 id="a-small-bus-is-enough">A small bus is enough</h2>
<p>I do not start with a giant framework.</p>
<p>I usually want something close to this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IEventBus</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    IDisposable Subscribe&lt;T&gt;(Action&lt;T&gt; handler);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Publish&lt;T&gt;(T message);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is enough for most gameplay code.</p>
<p>The subscription returns <code>IDisposable</code> so the listener can detach cleanly. I like that because it makes lifetime obvious. When the listener dies, the subscription should die too.</p>
<p>A tiny implementation looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">EventBus</span> : IEventBus
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> Dictionary&lt;Type, List&lt;Delegate&gt;&gt; _subscribers = <span style="color:#66d9ef">new</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> IDisposable Subscribe&lt;T&gt;(Action&lt;T&gt; handler)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (!_subscribers.TryGetValue(<span style="color:#66d9ef">typeof</span>(T), <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> list))
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            list = <span style="color:#66d9ef">new</span> List&lt;Delegate&gt;();
</span></span><span style="display:flex;"><span>            _subscribers[<span style="color:#66d9ef">typeof</span>(T)] = list;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        list.Add(handler);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> Subscription(() =&gt; list.Remove(handler));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Publish&lt;T&gt;(T message)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (!_subscribers.TryGetValue(<span style="color:#66d9ef">typeof</span>(T), <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> list))
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Ceiling: this copies the listener list so publish stays safe if a handler unsubscribes mid-flight.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// If this becomes hot, split the bus by feature or move to a type-specific dispatch path.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> snapshot = list.ToArray();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i = <span style="color:#ae81ff">0</span>; i &lt; snapshot.Length; i++)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            ((Action&lt;T&gt;)snapshot[i]).Invoke(message);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Subscription</span> : IDisposable
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">private</span> Action? _dispose;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> Subscription(Action dispose)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            _dispose = dispose;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Dispose()
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            Interlocked.Exchange(<span style="color:#66d9ef">ref</span> _dispose, <span style="color:#66d9ef">null</span>)?.Invoke();
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is not the only possible shape, and it is not the final word. It is the smallest version I am willing to carry in a real project.</p>
<p>If a team wants a more explicit setup, I will often break this into message channels or feature-specific buses. If a project is tiny, I may even use plain C# events instead.</p>
<p>The point is not to worship the bus.
The point is to make communication cheaper to change.</p>
<h2 id="the-lifetime-rule-that-saves-me-from-pain">The lifetime rule that saves me from pain</h2>
<p>The biggest mistake I see with buses is not the dispatch code. It is the lifetime story.</p>
<p>If a listener subscribes and never unsubscribes, the bus can keep it alive longer than intended. That is how a clean pattern turns into a memory leak or a scene bug that only appears after a restart.</p>
<p>So I keep one rule in mind:</p>
<p><strong>subscribe on enable, unsubscribe on disable, or dispose the token when the owner goes away.</strong></p>
<p>In Unity code, that usually looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">HudScoreListener</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> ScoreView scoreView = <span style="color:#66d9ef">null</span>!;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> IEventBus _bus = <span style="color:#66d9ef">null</span>!;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> IDisposable? _subscription;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Construct(IEventBus bus)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _bus = bus;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnEnable()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _subscription = _bus.Subscribe&lt;ScoreChanged&gt;(OnScoreChanged);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnDisable()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _subscription?.Dispose();
</span></span><span style="display:flex;"><span>        _subscription = <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnScoreChanged(ScoreChanged message)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        scoreView.SetScore(message.Value);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is boring code, which is exactly what I want here.</p>
<p>The bus can only stay tidy if ownership is obvious. If the project uses a scene lifetime scope, I keep the bus inside that scope. If the project has a persistent application root, I keep the bus there and treat it like a real long-lived service, not a magical global.</p>
<p>I do not default to a static bus unless the project is extremely small and I am willing to pay the maintenance cost later.</p>
<h2 id="the-other-lifetime-rule-events-are-not-state">The other lifetime rule: events are not state</h2>
<p>This one trips people up because the names sound similar.</p>
<p>An event says something changed.
A state object says what is true now.</p>
<p>Those are different tools.</p>
<p>If a UI element joins late, it should not need to replay a pile of old events just to figure out the current value of health, ammo, or quest progress. That information belongs in state.</p>
<p>The event bus should carry transitions:</p>
<ul>
<li>enemy died</li>
<li>item picked up</li>
<li>inventory changed</li>
<li>objective completed</li>
<li>scene entered</li>
</ul>
<p>The state holder should answer present-tense questions:</p>
<ul>
<li>how much health remains</li>
<li>how many coins the player has</li>
<li>which quest is active</li>
<li>which menu is open</li>
</ul>
<p>If I mix those two responsibilities, I get a bus that starts acting like a hidden database. Then every new subscriber has to ask, &ldquo;Did I miss the important thing earlier?&rdquo; That is not a bus problem. That is a design problem.</p>
<h2 id="what-i-use-it-for-in-practice">What I use it for in practice</h2>
<p>There are a few places where I reach for a bus quickly:</p>
<ul>
<li>combat and damage notifications</li>
<li>quest or objective progression</li>
<li>inventory and currency changes</li>
<li>UI reactions to gameplay changes</li>
<li>audio triggers that should not live inside gameplay code</li>
<li>editor tooling that wants to listen to gameplay state without owning it</li>
</ul>
<p>Those are all places where a message is often better than a direct dependency.</p>
<p>A typical example is a damage flow.</p>
<p>The health component owns the actual numbers.
When health reaches zero, it publishes a death event.
A spawn system may listen and schedule a respawn.
The score system may listen and award points.
The UI may listen and flash a red frame.
The audio system may listen and play a sting.</p>
<p>None of those subscribers should be making decisions about health itself. They should be responding to the result.</p>
<p>That separation keeps combat code focused.</p>
<h2 id="where-i-stop-and-use-something-simpler">Where I stop and use something simpler</h2>
<p>I do not use an event bus for everything.</p>
<p>If two systems are local and stable, a direct reference is usually easier to follow.
If one system owns another, I often prefer a method call.
If a feature is purely procedural, a bus can be the wrong shape entirely.</p>
<p>I also avoid using a bus to hide bad ownership. Sometimes the reason two objects are talking so much is that they should be a single object, or a nested subsystem, or a small state machine. A bus can silence the smell without fixing it.</p>
<p>That is the lazy mistake.</p>
<p>Good architecture removes unnecessary coupling.
Bad architecture just moves it out of sight.</p>
<h2 id="the-three-failure-modes-i-watch-for">The three failure modes I watch for</h2>
<p>When event buses go wrong, they usually go wrong in one of three ways.</p>
<h3 id="1-everything-becomes-an-event">1. Everything becomes an event</h3>
<p>If every method turns into a message, the code stops expressing intent.
The bus becomes a dumping ground for everything the team does not want to model directly.</p>
<p>That is not decoupling. That is avoidance.</p>
<h3 id="2-the-bus-becomes-global-state">2. The bus becomes global state</h3>
<p>A static bus is tempting because it is convenient.
It is also dangerously invisible.</p>
<p>Now any code in the project can publish anything at any time. That makes the dependency graph harder to see and the test setup harder to control. If I have to reach for a global, I want to be sure the gain is worth the fog.</p>
<h3 id="3-subscribers-forget-to-leave">3. Subscribers forget to leave</h3>
<p>This is the classic leak.
The object dies, but the bus still holds a delegate, so something that should be gone is still reachable.</p>
<p>Unity scene changes are especially good at exposing this kind of mistake in ugly ways. If the listener is tied to a <code>MonoBehaviour</code>, I make unsubscribing part of the component lifecycle, not an afterthought.</p>
<h2 id="unity-specific-tradeoffs">Unity-specific tradeoffs</h2>
<p>Unity gives a few different ways to model loose communication.</p>
<p>C# events are the cleanest option when the code owns the relationship and lifetime is clear.
<code>UnityEvent</code> is useful when I want inspector wiring or designer-driven setup.
<code>ScriptableObject</code> channels can be handy when I want a data asset to stand in for a message channel and expose it through the editor.</p>
<p>Those tools overlap, but they are not the same.</p>
<p>For code-heavy gameplay systems, I usually prefer plain C# events or a typed event bus because the flow stays explicit and the subscription lifecycle is straightforward.</p>
<p>For content-heavy workflows, I might use <code>UnityEvent</code> or a <code>ScriptableObject</code> channel if the editor integration is worth it.</p>
<p>The tradeoff is simple:</p>
<ul>
<li>more editor convenience usually means more hidden wiring</li>
<li>more explicit code usually means easier tracing and debugging</li>
</ul>
<p>I pick the one that matches the team’s pain, not the one that sounds elegant in a vacuum.</p>
<h2 id="debugging-and-tracing-are-part-of-the-design">Debugging and tracing are part of the design</h2>
<p>A bus is only good if I can still answer &ldquo;who reacted to this?&rdquo;</p>
<p>If I cannot trace the flow, I have not simplified anything. I have just made the code harder to follow.</p>
<p>So I keep a few habits:</p>
<ul>
<li>use message types with clear names</li>
<li>keep messages small and specific</li>
<li>avoid giant generic payloads</li>
<li>log selectively in debug builds if the flow is tricky</li>
<li>avoid publishing from places that already have an obvious direct caller</li>
</ul>
<p>I also try to keep the bus boundary narrow. The more systems the bus touches, the more careful I have to be about naming, ownership, and lifetime.</p>
<p>That is why I prefer a few clear buses or channels over one giant one when a project grows.</p>
<h2 id="the-shape-i-reach-for">The shape I reach for</h2>
<p>If I had to reduce the pattern to one sentence, it would be this:</p>
<p><strong>Use an event bus when one thing needs to announce a change to many things, and none of those listeners should own the sender.</strong></p>
<p>That rule is boring, but it is reliable.</p>
<p>Once I start using the bus to mask ownership problems, I am already losing.
Once I use it as a broadcast layer for genuine cross-cutting notifications, it pays for itself quickly.</p>
<p>I keep the sender focused.
I keep the listeners separate.
I keep the message small.
I keep the lifetime explicit.</p>
<p>That is enough.</p>
<h2 id="takeaway">Takeaway</h2>
<p>I do not use event buses because they are fashionable. I use them when they make communication easier to change without making ownership harder to see.</p>
<p>If I keep the bus scoped, typed, and disposable, it stays useful.
If I let it become global state or a dumping ground for every relationship in the game, it turns into the same mess I was trying to avoid.</p>
<h2 id="references">References</h2>
<p>These sources shaped the framing and tradeoffs in this post:</p>
<ul>
<li>Microsoft Learn, <a href="https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/events/">Events - C#</a></li>
<li>Unity Manual, <a href="https://docs.unity3d.com/Manual/UnityEvents.html">Inspector-configurable custom events</a></li>
<li>Game Programming Patterns, <a href="https://gameprogrammingpatterns.com/observer.html">Observer</a></li>
<li>Unity Manual, <a href="https://docs.unity3d.com/Manual/class-ScriptableObject.html">ScriptableObject</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Hierarchical State Machines in Unity: Keep the Top Level Boring</title>
            <link>https://blog.jonmikeys.com/posts/hierarchical-state-machines-in-unity/</link>
            <pubDate>Wed, 29 Jul 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/hierarchical-state-machines-in-unity/</guid>
            <description>A practical way to keep Unity state machines readable when several states share the same rules.</description>
            <content type="html"><![CDATA[<p>I reach for a finite state machine when a system has clear modes and clear transitions.</p>
<p>That works well until the graph starts to copy itself. A flat state machine is fine for a small character controller, a menu, or a short AI loop. Then the shared rules show up: pause input, damage reactions, locomotion, weapon handling, animation, camera control. At that point the machine is no longer just a machine. It is a pile of repeated rules with arrows drawn between them.</p>
<p>Hierarchical state machines solve that by letting a state own other states. The parent handles shared behavior. The child handles the part that is specific.</p>
<p>That sounds small. In practice it saves a lot of code.</p>
<h2 id="why-i-bother-with-hierarchy-at-all">Why I bother with hierarchy at all</h2>
<p>The basic reason is transition count.</p>
<p>If every state needs to know about every other state, the graph grows fast. A few states are fine. A dozen starts to hurt. What really gets ugly is not the number of states by itself, it is the number of special cases that keep leaking across them.</p>
<p>A hierarchy lets me move repeated rules upward.</p>
<p>Instead of this:</p>
<ul>
<li><code>GroundedIdle</code></li>
<li><code>GroundedRun</code></li>
<li><code>GroundedAim</code></li>
<li><code>AirborneJump</code></li>
<li><code>AirborneFall</code></li>
<li><code>AirborneAim</code></li>
<li><code>ClimbingIdle</code></li>
<li><code>ClimbingMove</code></li>
</ul>
<p>I can split the machine into layers:</p>
<ul>
<li><code>Exploration</code>
<ul>
<li><code>Grounded</code></li>
<li><code>Airborne</code></li>
<li><code>Climbing</code></li>
</ul>
</li>
<li><code>Combat</code>
<ul>
<li><code>Aiming</code></li>
<li><code>Attacking</code></li>
<li><code>Recovering</code></li>
</ul>
</li>
</ul>
<p>The parent owns the shared rules for a mode. The child owns the specifics. If I need to change what happens when the player is in exploration mode, I do it once instead of three or four times.</p>
<p>That is the real win: fewer places to forget.</p>
<h2 id="the-shape-i-keep-in-my-head">The shape I keep in my head</h2>
<p>Here is the simplest useful model.</p>
<ul>
<li>A <strong>state</strong> can handle an event or ignore it.</li>
<li>If the state ignores it, the parent gets a chance.</li>
<li>If the child state does not need special setup or teardown, the parent can do it.</li>
<li>If several children share a default, that default belongs in the parent.</li>
</ul>
<p>That last point is the one that saves code.</p>
<p>A hierarchical machine is not just a way to make the graph look tidy. It is a way to define ownership.</p>
<p>The child owns the exact behavior.
The parent owns the fallback.
The machine owns the transition.</p>
<p>That separation is what makes the design work.</p>
<p><img src="/images/posts/hierarchical-state-machines-in-unity/diagram.svg" alt="Hierarchical state machine diagram"></p>
<p>The diagram above is the version I keep reaching for in player code: a small top-level mode, a couple of nested substates, and a clear rule for what happens when a child state does not know how to handle something.</p>
<h2 id="a-useful-unity-rule-gameplay-logic-and-animation-logic-are-not-the-same-thing">A useful Unity rule: gameplay logic and animation logic are not the same thing</h2>
<p>Unity already gives me state machines in the Animator. It also gives me sub-state machines, entry transitions, and exit transitions, which is a nice editor-side way to group related animation states.</p>
<p>That part is useful.</p>
<p>But I do not want to confuse the animation graph with the game rules.</p>
<p>Animation can reflect the current mode. Gameplay should decide the current mode. If I mix the two, I end up with animation states quietly controlling things that should have been explicit in code.</p>
<p>I prefer to keep the gameplay state machine in plain C# and let the Animator follow along. The Animator is good at presentation. It is not the right place for the whole game loop to hide.</p>
<p>Unity’s own manual makes the same structural point in a different context: nested state machines are there to group related states and keep a large controller manageable. That is the same reason I use hierarchy in code. The editor feature is not the design. It is just one place the design shows up.</p>
<h2 id="how-i-model-the-parent-child-relationship">How I model the parent-child relationship</h2>
<p>I keep the code boring on purpose.</p>
<p>A child state needs to do two jobs:</p>
<ol>
<li>handle its own behavior</li>
<li>fall back to the parent when it does not care about an event</li>
</ol>
<p>That can be as simple as this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IState</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Enter();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Tick(<span style="color:#66d9ef">float</span> dt);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">bool</span> Handle(GameEvent evt);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Exit();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">State</span> : IState
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">readonly</span> State? Parent;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> State(State? parent)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Parent = parent;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">virtual</span> <span style="color:#66d9ef">void</span> Enter() { }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">virtual</span> <span style="color:#66d9ef">void</span> Tick(<span style="color:#66d9ef">float</span> dt) { }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">virtual</span> <span style="color:#66d9ef">void</span> Exit() { }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">virtual</span> <span style="color:#66d9ef">bool</span> Handle(GameEvent evt)
</span></span><span style="display:flex;"><span>        =&gt; Parent?.Handle(evt) ?? <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A child overrides <code>Handle</code> when it has a special case.
If it does not, the event keeps moving up.</p>
<p>That is the important part. I do not need a giant switch statement in the leaf state to keep re-implementing the same pause key, the same damage flash, or the same movement lock.</p>
<p>A parent can catch those once.</p>
<h2 id="a-player-controller-example">A player controller example</h2>
<p>The clearest place I have used this is a player controller.</p>
<p>At the top level, I usually care about a few broad modes:</p>
<ul>
<li><code>Exploration</code></li>
<li><code>Combat</code></li>
<li><code>UIBlocked</code></li>
<li><code>Dead</code></li>
</ul>
<p>Those modes change the rules in a way the rest of the game can feel. In <code>Exploration</code>, movement is free, camera control is normal, and the player can interact with the world. In <code>Combat</code>, the camera may change, input may be narrower, and the weapon rules matter. In <code>UIBlocked</code>, most of the game should stop listening except for menu navigation. In <code>Dead</code>, almost everything is a no-op.</p>
<p>Inside <code>Exploration</code>, I can keep the lower-level movement split separate:</p>
<ul>
<li><code>Grounded</code></li>
<li><code>Airborne</code></li>
<li><code>Climbing</code></li>
<li><code>Swimming</code></li>
</ul>
<p>Inside <code>Combat</code>, I can split by action:</p>
<ul>
<li><code>Aiming</code></li>
<li><code>Firing</code></li>
<li><code>Reloading</code></li>
<li><code>Recovering</code></li>
</ul>
<p>Those child states all share the same parent rules.</p>
<p>For example, <code>Aiming</code> and <code>Firing</code> may both allow the same pause input. <code>Grounded</code> and <code>Airborne</code> may both use the same footstep sound handling or camera bob reset. <code>Climbing</code> and <code>Swimming</code> may both ignore jump buffering and use a different gravity rule.</p>
<p>If I do not have hierarchy, each of those child states ends up carrying copies of the shared logic. That is where maintenance gets expensive. I do not want to debug the same input rule in four places because the state graph was flat and looked harmless at the time.</p>
<h2 id="shared-defaults-belong-in-the-parent">Shared defaults belong in the parent</h2>
<p>This is where a lot of state machine code gets messy.</p>
<p>A child should not carry code that is obviously common to its siblings. If three states all react to damage the same way, damage belongs one level higher.</p>
<p>That usually means these things want to live in the parent:</p>
<ul>
<li>shared animation parameters</li>
<li>shared input locks</li>
<li>shared movement restrictions</li>
<li>shared camera defaults</li>
<li>shared audio cues</li>
<li>shared exit cleanup</li>
</ul>
<p>The parent is the place for the default response.
The child is the place for the exception.</p>
<p>That boundary keeps me honest. If I find myself copying the same code into several sibling states, I usually stop and ask whether the rule is actually a parent rule.</p>
<p>Most of the time it is.</p>
<h2 id="let-unhandled-events-bubble-upward">Let unhandled events bubble upward</h2>
<p>The nicest part of a hierarchical machine is also the part that saves me the most time: unhandled events can bubble up.</p>
<p>If the child does not know what to do with an input, the parent gets the chance to decide.</p>
<p>That sounds trivial, but it is what makes the machine feel layered instead of duplicated. A <code>Grounded</code> state does not need to know how to pause the game if <code>Exploration</code> already knows that. A <code>Reloading</code> state does not need to know how to show the inventory overlay if <code>Combat</code> or <code>PlayerRoot</code> already handles it.</p>
<p>That means I can add a new child state without re-implementing every global rule.</p>
<p>This is also where hierarchy differs from a pure “one big switch” approach. In a switch, every case tends to become responsible for everything. In a hierarchy, responsibility has a path.</p>
<p>That path is the design.</p>
<h2 id="enter-and-exit-need-to-be-predictable">Enter and exit need to be predictable</h2>
<p>Once the graph has parents, entry and exit matter more.</p>
<p>I want to be strict about this:</p>
<ul>
<li>entering a parent should set up shared state only once</li>
<li>entering a child should set up child-specific state only</li>
<li>exiting a child should undo only child-specific state</li>
<li>exiting a parent should clean up the shared state it owns</li>
</ul>
<p>If I get sloppy here, the hierarchy becomes fragile very quickly. The bug usually looks like a state that enters correctly the first time and then behaves oddly after a couple of transitions because some shared value was not reset where it should have been.</p>
<p>That is why I prefer a machine that owns the current path and computes the transition deliberately instead of letting every state manually poke at siblings.</p>
<p>If the machine changes from <code>Exploration.Grounded</code> to <code>Combat.Aiming</code>, I want the transition to know what part of the path is shared, what part needs to exit, and what part needs to enter.</p>
<p>A state should not have to guess.</p>
<h2 id="where-unitys-built-in-tools-fit">Where Unity’s built-in tools fit</h2>
<p>I do use Unity’s Animator hierarchy for animation when it makes sense.</p>
<p>A sub-state machine is good when the animation graph itself has a clear internal structure. A locomotion group is not the same as a combat group. A menu animation set is not the same as a character locomotion set. Grouping those in the editor makes the controller easier to read.</p>
<p>Unity’s documentation on animation state machines, nested state machines, and state machine transitions is useful here because it shows the same logic at the animation layer: group related states, use entry and exit transitions to keep the graph readable, and do not let one giant controller turn into a tangle.</p>
<p>That said, I still keep the real gameplay decisions in code.</p>
<p>I have seen too many projects let the Animator become the hidden source of truth. The result is usually a system that looks fine in the editor but is miserable to debug when someone asks, “Why did the player stop accepting input?”</p>
<p>I want gameplay logic in code and animation as a follower. That split keeps the bug surface smaller.</p>
<h2 id="when-i-do-not-use-a-hierarchy">When I do not use a hierarchy</h2>
<p>Hierarchical state machines are useful, but they are not free.</p>
<p>I do not reach for one when:</p>
<ul>
<li>the machine only has a few states</li>
<li>the shared behavior is tiny</li>
<li>the system is really two independent systems, not one nested one</li>
<li>the hierarchy would hide a design problem instead of solving it</li>
</ul>
<p>If I only have three states, a flat machine is usually easier.
If I have locomotion and weapon selection and UI focus all tangled together, hierarchy may not be the real fix. Separate machines or composition may be cleaner.</p>
<p>This matters because hierarchy can look elegant while still being wrong. It is very easy to make the tree pretty and still have the architecture be confused.</p>
<p>A hierarchy is good when it expresses real ownership.
It is bad when it is used as a dustbin for extra states.</p>
<h2 id="a-practical-rule-of-thumb">A practical rule of thumb</h2>
<p>I start flat.</p>
<p>When I see repeated transitions, repeated shared rules, or repeated cleanup, I split the machine into a parent state and a set of children.</p>
<p>If the parent stays small and boring, the split was probably the right move.</p>
<p>If the parent starts turning into a second monster, I stop and look for a different boundary. Sometimes the problem is not that I need a deeper state machine. Sometimes the problem is that I need two machines, or a separate component, or a simpler rule altogether.</p>
<p>That is usually the right question to ask in game code: am I building structure, or am I only moving complexity around?</p>
<h2 id="takeaway">Takeaway</h2>
<p>Hierarchical state machines are worth using when several states share the same rules and the flat graph is starting to duplicate itself.</p>
<p>Use the parent for defaults, the child for specifics, and let unhandled events bubble upward. Keep gameplay decisions in code, use Unity’s animation hierarchy where it helps the presentation layer, and do not force a hierarchy where a flat machine or separate systems would be simpler.</p>
<p>If the top level stays boring, the whole machine usually stays understandable.</p>
<h2 id="references">References</h2>
<ul>
<li>Robert Nystrom, <em>Game Programming Patterns</em> — State: <a href="https://gameprogrammingpatterns.com/state.html">https://gameprogrammingpatterns.com/state.html</a></li>
<li>Unity Manual — Animation state machine: <a href="https://docs.unity3d.com/6000.5/Documentation/Manual/AnimationStateMachines.html">https://docs.unity3d.com/6000.5/Documentation/Manual/AnimationStateMachines.html</a></li>
<li>Unity Manual — Nested state machines: <a href="https://docs.unity3d.com/6000.5/Documentation/Manual/NestedStateMachines.html">https://docs.unity3d.com/6000.5/Documentation/Manual/NestedStateMachines.html</a></li>
<li>Unity Manual — State machine transitions: <a href="https://docs.unity3d.com/6000.5/Documentation/Manual/StateMachineTransitions.html">https://docs.unity3d.com/6000.5/Documentation/Manual/StateMachineTransitions.html</a></li>
<li>Unity Manual — StateMachineBehaviour: <a href="https://docs.unity3d.com/6000.5/Documentation/Manual/StateMachineBehaviours.html">https://docs.unity3d.com/6000.5/Documentation/Manual/StateMachineBehaviours.html</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Command Pattern in Games: Queue the Intent, Not the Button</title>
            <link>https://blog.jonmikeys.com/posts/command-pattern-in-games/</link>
            <pubDate>Sat, 25 Jul 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/command-pattern-in-games/</guid>
            <description>A practical look at the command pattern in game code, with examples for input, undo, replay, and when a direct call is still better.</description>
            <content type="html"><![CDATA[<p>The command pattern shows up in game code when an action needs to travel before it happens.</p>
<p>A button press, a menu click, a network packet, or an AI order often starts as the same thing: a request for an action. The request does not always belong next to the code that performs the action. Sometimes the request needs to wait until the right frame. Sometimes it needs to be recorded for replay. Sometimes it needs to be undone. Sometimes it needs to cross a system boundary without dragging the whole gameplay layer along with it.</p>
<p>That is the useful shape of the command pattern. It turns intent into an object so the intent can move around as data instead of being trapped inside a direct method call.</p>
<p><img src="/images/posts/command-pattern-in-games/diagram.svg" alt="Command pattern flow"></p>
<h2 id="what-the-pattern-actually-is">What the pattern actually is</h2>
<p>At its core, the command pattern is a request wrapped in an object.</p>
<p>That object usually carries three parts:</p>
<ul>
<li>the action to perform</li>
<li>the data needed to perform it</li>
<li>the receiver that owns the real work</li>
</ul>
<p>That sounds formal, but the idea stays simple in practice. Instead of calling <code>character.Jump()</code> from every input path, a project can create a <code>JumpCommand</code> and pass that command to a buffer, queue, recorder, or input handler. The command becomes the unit that moves through the system.</p>
<p>That extra layer is not free, so the pattern only earns its keep when the request needs more than immediate execution. Games run into that situation constantly.</p>
<h2 id="why-games-reach-for-commands-so-often">Why games reach for commands so often</h2>
<p>Games are full of actions that are easy to describe and awkward to place.</p>
<p>A player presses a button. A UI menu submits an order. A bot decides to attack. A replay file needs to reconstruct a session. A networked simulation wants a small deterministic message instead of a loose pile of state changes. These problems are all different on the surface, but they share one trait: the action should survive outside the place where it was first noticed.</p>
<p>The command pattern helps because it separates <strong>request</strong> from <strong>execution</strong>.</p>
<p>That separation buys a few practical benefits:</p>
<ul>
<li><strong>Rebindable input</strong> — many inputs can map to the same command.</li>
<li><strong>Input buffering</strong> — commands can wait until the right frame or turn.</li>
<li><strong>Undo and redo</strong> — commands can store enough information to reverse themselves.</li>
<li><strong>Replay</strong> — a stream of commands is often easier to store than raw device state.</li>
<li><strong>Networking</strong> — commands can act as compact messages between clients and simulation.</li>
<li><strong>AI orders</strong> — the same action shape can work for player input, scripted behavior, or bot decisions.</li>
</ul>
<p>That list is the real reason the pattern appears in game architecture books and engine samples. It is not about ceremony. It is about keeping intent portable.</p>
<h2 id="a-small-c-version">A small C# version</h2>
<p>The classic version is tiny.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ICommand</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Execute();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">JumpCommand</span> : ICommand
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ICharacter character;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> JumpCommand(ICharacter character)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.character = character;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Execute()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        character.Jump();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That example is almost comically small, which is a good sign. A command should not feel like a mini-framework. It should feel like a small object that says, “perform this action later.”</p>
<p>A buffer that runs commands can stay just as small:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CommandBuffer</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> Queue&lt;ICommand&gt; pending = <span style="color:#66d9ef">new</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Enqueue(ICommand command)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        pending.Enqueue(command);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> RunAll()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">while</span> (pending.Count &gt; <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            pending.Dequeue().Execute();
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That version is deliberately boring. Boring code is useful when the goal is to make intent visible without pulling in more machinery than necessary.</p>
<h2 id="input-is-the-obvious-place-to-start">Input is the obvious place to start</h2>
<p>The easiest way to understand the pattern is through input.</p>
<p>A game usually has at least three layers around input:</p>
<ol>
<li>the device or UI event that noticed something happened</li>
<li>the mapping layer that translates that event into game intent</li>
<li>the gameplay layer that performs the actual action</li>
</ol>
<p>Directly wiring button callbacks into gameplay code can work for a prototype, but it gets brittle quickly. The button knows too much. The gameplay code ends up scattered across input paths. Rebinding becomes annoying. Automated testing gets harder. And once a second source of input appears, the wiring starts to multiply.</p>
<p>A command object gives the mapping layer something stable to emit. The button does not need to know how a jump works. It only needs to produce a jump command.</p>
<p>Unity’s Input System already nudges projects in that direction. An action can be performed, rebound, and routed without tying the control directly to a specific gameplay method. That makes it a comfortable home for commands or command-like adapters.</p>
<p>The useful mental model is simple: the input layer decides <strong>what was meant</strong>; the command says <strong>what should happen</strong>; the receiver owns <strong>how it happens</strong>.</p>
<h2 id="command-objects-make-boundaries-cleaner">Command objects make boundaries cleaner</h2>
<p>The best use of the command pattern is usually at a boundary.</p>
<p>That boundary might be:</p>
<ul>
<li>input code handing intent to gameplay code</li>
<li>UI code triggering game systems</li>
<li>editor tools changing state in a way that can be undone</li>
<li>replay systems recording player actions</li>
<li>network code turning messages into deterministic simulation steps</li>
</ul>
<p>Each of those cases shares the same pressure: one side should not know too much about the other side’s internals.</p>
<p>A direct method call often crosses that boundary too eagerly. It works, but it encourages the caller to reach into the receiver’s details. Over time, the codebase starts to feel local while behaving globally. A click in one place touches state in five others. A small change becomes a cascade.</p>
<p>A command object narrows that seam. The caller produces an intent. The receiver performs the work. The command sits in the middle and keeps the contract small.</p>
<p>That extra object is useful because it reduces how many systems have to know each other’s exact shape.</p>
<h2 id="undo-and-redo-need-commands-for-a-reason">Undo and redo need commands for a reason</h2>
<p>Undo is where the command pattern stops being a neat input trick and starts looking like a real architecture tool.</p>
<p>A direct method call is easy to make, but a reverse operation is not always obvious. If a move command changes position, undo needs the previous position. If a purchase command spends currency, undo needs the amount and the prior balance. If an inventory command moves items, undo needs the source, destination, and item counts.</p>
<p>Commands are a good place to store that reversal data.</p>
<p>A command can record what it needs before it executes. That gives undo a clear path back to the previous state. Redo then becomes another execution of the same intent, often with the same captured data.</p>
<p>That does not mean every game needs a full command history. It only means the pattern is a natural fit when reversibility matters. An editor, strategy game, city builder, or turn-based game often benefits from this immediately. A twitch shooter usually cares less.</p>
<h2 id="replay-and-networking-benefit-from-the-same-idea">Replay and networking benefit from the same idea</h2>
<p>Replay systems and networked games often want a stream of actions instead of a stream of state snapshots.</p>
<p>A command stream is compact, readable, and easier to reason about than a full dump of every system at every frame. The game can record “move left,” “fire,” “open door,” or “end turn” rather than trying to serialize the entire world after every update.</p>
<p>That approach has two advantages:</p>
<ul>
<li>the log is often much smaller than full state capture</li>
<li>the sequence of actions can be re-simulated deterministically if the simulation is stable enough</li>
</ul>
<p>That second part is the important one. Command-driven replay is only as good as the underlying simulation. If random numbers, floating-point drift, or hidden side effects are uncontrolled, the replay will diverge. The command stream is not magic; it just gives the game a cleaner set of inputs to reproduce.</p>
<p>For networking, the same logic applies. A command can stand in for the user’s intention, while the simulation on each machine applies those commands in the agreed order. The smaller and more deterministic the command, the easier the system is to keep honest.</p>
<h2 id="a-practical-example-with-player-input">A practical example with player input</h2>
<p>Suppose a character can jump, dash, and attack. The game needs the same actions to be reachable from keyboard input, gamepad input, and maybe AI control later.</p>
<p>A simple setup might look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ICharacter</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Jump();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Dash();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Attack();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ICommand</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Execute();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DashCommand</span> : ICommand
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ICharacter character;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> DashCommand(ICharacter character)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.character = character;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Execute()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        character.Dash();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">AttackCommand</span> : ICommand
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ICharacter character;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> AttackCommand(ICharacter character)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.character = character;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Execute()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        character.Attack();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then the input adapter can map controls to command instances:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerInputAdapter</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> CommandBuffer buffer;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ICharacter character;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> PlayerInputAdapter(CommandBuffer buffer, ICharacter character)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.buffer = buffer;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.character = character;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> OnJumpPressed()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        buffer.Enqueue(<span style="color:#66d9ef">new</span> JumpCommand(character));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> OnDashPressed()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        buffer.Enqueue(<span style="color:#66d9ef">new</span> DashCommand(character));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> OnAttackPressed()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        buffer.Enqueue(<span style="color:#66d9ef">new</span> AttackCommand(character));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That arrangement keeps the input code narrow. The adapter translates controls into commands. The game loop decides when to run them. The character object still owns the actual behavior.</p>
<p>There is a hidden benefit here: the same command types can be created by a bot, a script, or a network message without inventing a second action system.</p>
<h2 id="queueing-is-useful-but-not-every-queue-needs-a-framework">Queueing is useful, but not every queue needs a framework</h2>
<p>A lot of command-pattern examples drift into overengineering the moment a queue appears.</p>
<p>A simple FIFO queue is often enough. Player input usually does not need a priority heap, a dependency graph, or a tiny scheduling language. If a command arrives too early, it can wait one frame. If it needs to be deferred longer, a timestamp can handle that. If the game eventually needs different lanes for combat, UI, and simulation, those lanes can be added when the problem proves they are necessary.</p>
<p>That is the lazy-but-correct way to treat the pattern: keep the command shape small until the need becomes real.</p>
<p>A command is not automatically better because it is wrapped in an object. It is better when the wrapper solves a specific boundary problem.</p>
<h2 id="when-a-direct-call-is-still-the-better-choice">When a direct call is still the better choice</h2>
<p>The command pattern is useful, but it is not a replacement for normal method calls.</p>
<p>If an action is immediate, local, and never needs to be queued, replayed, undone, or routed through another layer, a plain method call is usually cleaner. That path is easier to read, easier to test, and easier to change later.</p>
<p>That matters because command objects can become noise when used everywhere. A project does not need a command for every helper method, every animation trigger, or every internal system transition. At that point the design stops clarifying the code and starts hiding it.</p>
<p>A good rule is simple:</p>
<ul>
<li>use a direct call when the request and the execution belong together</li>
<li>use a command when the request has to survive beyond the current call site</li>
</ul>
<p>That distinction keeps the pattern useful instead of theatrical.</p>
<h2 id="commands-versus-events">Commands versus events</h2>
<p>Commands and events look similar at a glance because both can travel through a system, but they serve different purposes.</p>
<p>A command says, “do this.”</p>
<p>An event says, “this already happened.”</p>
<p>That distinction shapes how each one should be used.</p>
<p>A jump command is an instruction. A landed event is a fact. A purchase command requests a change. A gold-collected event reports a completed change. Mixing the two tends to blur ownership and make systems feel less predictable.</p>
<p>The simplest way to keep the design sane is to let commands flow toward action and events flow toward observation.</p>
<p>That separation pays off when the codebase grows. The input layer can emit commands without pretending something has already happened. The simulation can raise events after state changes without pretending the caller still controls the result.</p>
<h2 id="common-variants-worth-knowing">Common variants worth knowing</h2>
<p>Several variations show up often enough to be worth naming.</p>
<h3 id="command-with-parameters">Command with parameters</h3>
<p>Some commands carry small argument sets instead of a full receiver reference. A move command might hold a direction vector and speed multiplier. A build command might hold a location, a building type, and a cost.</p>
<p>That works well when the command is also useful as a data packet.</p>
<h3 id="composite-commands">Composite commands</h3>
<p>Sometimes a single player action expands into several smaller actions. A skill cast might spend mana, spawn a projectile, and start a cooldown. A composite command can group those together so the higher-level action still reads as one intent.</p>
<p>That said, composite commands should stay honest. If the bundle starts behaving like a whole subsystem, the design probably needs another boundary.</p>
<h3 id="reversible-commands">Reversible commands</h3>
<p>Undoable systems often add <code>Undo()</code> alongside <code>Execute()</code>. That is useful when the command owns enough information to put the world back the way it was.</p>
<p>The important part is not the interface itself. The important part is whether the command can genuinely restore the previous state without guessing.</p>
<h3 id="serialized-commands">Serialized commands</h3>
<p>For replay or networking, the command may need to survive disk or the wire. In that case, it should stay small, explicit, and easy to version. The more it resembles a random object graph, the harder the format becomes to evolve.</p>
<h2 id="a-few-traps-to-avoid">A few traps to avoid</h2>
<p>The command pattern is easy to misuse because it feels tidy at first.</p>
<h3 id="trap-1-wrapping-everything">Trap 1: wrapping everything</h3>
<p>If every method call turns into a command, the codebase gains ceremony without gaining clarity. The simplest possible action should stay simple.</p>
<h3 id="trap-2-letting-commands-grow-into-mini-systems">Trap 2: letting commands grow into mini systems</h3>
<p>A command should remain a request. If it starts managing UI, animation, inventory, physics, and quest state all at once, the abstraction has failed.</p>
<h3 id="trap-3-building-a-queue-before-there-is-a-queue-problem">Trap 3: building a queue before there is a queue problem</h3>
<p>A command buffer is helpful when actions need timing or ordering. It is unnecessary when the action should happen now.</p>
<h3 id="trap-4-confusing-commands-with-events">Trap 4: confusing commands with events</h3>
<p>A request is not a fact. A fact is not a request. When those roles blur, the code becomes harder to debug.</p>
<h3 id="trap-5-assuming-command-streams-guarantee-determinism">Trap 5: assuming command streams guarantee determinism</h3>
<p>Replay and networking need deterministic simulation as much as they need a command log. The command pattern helps with the input side, not the whole problem.</p>
<h2 id="a-short-implementation-checklist">A short implementation checklist</h2>
<p>When a command pattern feels appropriate, the design usually stays healthier if the following questions are answered early:</p>
<ul>
<li>What boundary is the command crossing?</li>
<li>What needs to be stored with the intent?</li>
<li>Does the action need to be undoable?</li>
<li>Does it need to be queued, replayed, or serialized?</li>
<li>Can the command stay tiny, or is a direct call enough?</li>
</ul>
<p>Those questions are more useful than starting with a class diagram. They keep the pattern tied to the actual problem rather than to the idea of a pattern.</p>
<h2 id="the-practical-takeaway">The practical takeaway</h2>
<p>The command pattern is a good fit when intent needs to outlive the moment it was created.</p>
<p>In game code, that usually means input mapping, buffering, undo, replay, networking, or clean separation between systems. A command object gives those problems a stable shape. It turns “something should happen” into a small unit that can move, wait, repeat, or be reversed.</p>
<p>That is the part that makes the pattern valuable. It protects the game from tangled direct calls when the request has to travel.</p>
<p>It is also the part that keeps the pattern honest. When the request does not need to travel, a direct method call is still the better tool. Good architecture in games is often less about using the fanciest abstraction and more about choosing the smallest one that still survives the real constraints.</p>
<h2 id="references">References</h2>
<ul>
<li>Robert Nystrom, <a href="https://gameprogrammingpatterns.com/command.html">Command</a></li>
<li>Unity Manual, <a href="https://docs.unity3d.com/Packages/com.unity.inputsystem@1.10/manual/Actions.html">Input actions</a></li>
<li>Refactoring Guru, <a href="https://refactoring.guru/design-patterns/command">Command</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Raycasting: How Wolfenstein 3D Fakes 3D With 2D Math</title>
            <link>https://blog.jonmikeys.com/posts/raycasting/</link>
            <pubDate>Fri, 24 Jul 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/raycasting/</guid>
            <description>A practical rewrite of Lode&amp;rsquo;s raycasting tutorial, with the same core ideas and images explained in cleaner prose.</description>
            <content type="html"><![CDATA[<h2 id="introduction">Introduction</h2>
<p>Raycasting is one of those tricks that looks like magic until the geometry is written down. It creates the illusion of a three-dimensional world while still working entirely on a 2D grid. That made it a perfect fit for older computers, where full real-time 3D engines were far too expensive to run comfortably.</p>
<p>The classic example is <em>Wolfenstein 3D</em>. Its world was built from square tiles, and every wall had the same height. That seems extremely limited, and it was. It could not do stairs, slopes, or true vertical movement. But within those limits, the effect was fast enough and convincing enough to define a whole style of first-person game.</p>
<p><img src="/images/posts/raycasting/wolf3d.jpg" alt="Wolfenstein 3D screenshot"></p>
<p>The key idea is simple: the screen is not rendered as a full 3D scene. Instead, each vertical stripe is treated as a separate question. What wall does this slice of view hit, and how tall should it appear? The answer comes from 2D math, not a full 3D engine. That is why raycasting became so useful.</p>
<p>It is also worth separating raycasting from raytracing. They sound similar, but they solve different problems. Raycasting is a fast way to build a 3D-looking view from a 2D map. Raytracing is a lighting technique that simulates reflections, shadows, and more realistic light behavior in true 3D scenes. Raytracing looks richer, but it is far more expensive.</p>
<p>The nice part is that raycasting still teaches a lot. It touches grid traversal, vectors, camera projection, movement, collision, texture mapping, and a few performance tradeoffs that still matter in modern code.</p>
<h2 id="the-basic-idea">The basic idea</h2>
<p>The world is treated as a square grid. Each cell is either empty space or a wall. Empty cells are usually <code>0</code>. Any positive value means a wall tile, often with a different color or texture. The map is just data, and the renderer interprets that data one stripe at a time.</p>
<p>For each <code>x</code> position on the screen, a ray is fired from the player’s position into the world. The ray direction depends on the player’s facing direction and on the horizontal position on the screen. Once the ray enters a wall cell, the distance to that hit point is used to figure out how tall the wall should look on screen.</p>
<p>That single rule is what sells the whole illusion: the farther the wall, the shorter it appears. The closer the wall, the taller it appears.</p>
<p><img src="/images/posts/raycasting/raycastgrid.svg" alt="Two rays hitting walls on a grid"></p>
<p>A naive implementation would move the ray forward in tiny equal steps and check the map again and again. That works, but it is inefficient and can still miss a wall if the step is too large. Make the step smaller and the accuracy improves, but the number of checks grows quickly. In the limit, perfect precision would require infinitely many steps, which is not a good trade.</p>
<p>A better approach is to jump from grid boundary to grid boundary. Since the map is made of square cells, the ray can be advanced to the next vertical or horizontal edge in a controlled way. That is where the DDA method comes in. DDA stands for Digital Differential Analysis, and in this case it is just a fast way to walk a line through a square grid.</p>
<p><img src="/images/posts/raycasting/raycastdelta.svg" alt="Ray stepping through grid boundaries"></p>
<p>Instead of thinking in angles, the tutorial uses vectors and a camera plane. The player has a position vector and a direction vector. A second vector, the camera plane, sits perpendicular to the direction vector. The plane represents the width of the screen. Every ray is built from a blend of the forward direction and some portion of that plane.</p>
<p>That makes the math cleaner than juggling a bunch of angles. The direction vector says where the player is looking. The camera plane spreads rays left and right across the field of view.</p>
<h2 id="camera-direction-and-field-of-view">Camera, direction, and field of view</h2>
<p>The camera plane is the part that turns the 2D map into a camera-like view. It is not a true 3D plane here. In this setup, it is just a 2D vector that sits perpendicular to the direction vector. The direction vector points forward, and the plane stretches sideways.</p>
<p><img src="/images/posts/raycasting/raycastingcamera.svg" alt="Camera concept and ray directions"></p>
<p>The field of view depends on the relationship between the direction vector and the camera plane vector. If they have about the same length, the view is around 90 degrees wide. If the direction vector is longer, the view narrows. If the plane is longer, the view widens.</p>
<p><img src="/images/posts/raycasting/raycastingFOV90.svg" alt="FOV at 90 degrees"></p>
<p><img src="/images/posts/raycasting/raycastingFOV0.svg" alt="Narrower field of view"></p>
<p><img src="/images/posts/raycasting/raycastingFOV180.svg" alt="Wider field of view"></p>
<p>That is a useful control knob. A narrow field of view feels like zooming in. A wide one feels like zooming out. The engine does not need a special feature for that. It only needs a different balance between the direction vector and the plane vector.</p>
<p>When the player rotates, both vectors rotate together. That keeps the camera stable and prevents the whole scene from warping. The standard 2D rotation matrix handles it neatly. If the camera plane is not kept perpendicular to the direction vector, the world begins to look skewed.</p>
<p><img src="/images/posts/raycasting/raycastingskewed.svg" alt="A skewed world from a bad camera plane"></p>
<h2 id="untextured-raycaster">Untextured raycaster</h2>
<p>The untextured version is the simplest place to start because it strips the problem down to geometry. The map is a 2D array. The player starts in a fixed position with an initial direction vector and camera plane. A frame timer keeps movement speed consistent regardless of frame rate.</p>
<p>For each screen column, the renderer does the same routine:</p>
<ol>
<li>Compute the ray direction for that column.</li>
<li>Find the current map cell.</li>
<li>Set up the DDA stepping distances.</li>
<li>Walk through the grid until a wall is found.</li>
<li>Measure the perpendicular distance.</li>
<li>Convert distance into wall height.</li>
<li>Draw the stripe in a solid color.</li>
</ol>
<p>The current screen column is expressed as a camera-space <code>x</code> value ranging from <code>-1</code> on the left to <code>+1</code> on the right. The center of the screen is <code>0</code>. The ray direction is then the forward direction plus a scaled piece of the camera plane. That simple formula gives each screen column its own ray.</p>
<p>The DDA setup needs a few values. The player position is converted to the current map cell. The distances to the next x-side and y-side are tracked separately. The <code>deltaDistX</code> and <code>deltaDistY</code> values represent how much farther the ray must travel to move from one grid boundary to the next in each direction.</p>
<p>If one of the ray direction components is zero, the code uses a huge placeholder distance or infinity so the algorithm can keep working. That avoids division by zero without complicating the rest of the logic.</p>
<p>The step direction comes next. If the ray points left, the x-step is <code>-1</code>. If it points right, the x-step is <code>+1</code>. The same applies to the y direction. The starting side distance is set so the DDA knows how far it must go to reach the first wall boundary.</p>
<p>Then the loop begins. At each step, the algorithm compares the x-side distance and the y-side distance. Whichever is smaller tells the code which grid boundary comes next. The ray moves to that boundary, the map cell changes, and the tile is checked for a wall. Once a nonzero cell is found, the search ends.</p>
<p><img src="/images/posts/raycasting/raycasthit.svg" alt="A ray hitting a wall square"></p>
<p>The distance used for rendering is not the raw straight-line distance to the hit point. If raw distance is used, the walls bend at the sides of the screen, producing the fisheye effect. The correct measure is the perpendicular distance to the camera direction. That keeps the projection straight and natural.</p>
<p><img src="/images/posts/raycasting/raycastdist.svg" alt="Perpendicular distance explanation"></p>
<p>Once that distance is known, the wall height is just screen height divided by distance. A nearby wall becomes tall. A distant wall becomes short. The draw start and draw end positions are then clamped to the screen bounds so the stripe does not spill outside the visible area.</p>
<p>The wall type decides the color. A wall tile might map to red, green, blue, white, or another simple color. If the ray hit a y-side rather than an x-side, the color is darkened slightly. That creates a simple directional shading effect without any real lighting model.</p>
<p>The result is rough compared with textured graphics, but the world already feels like a real first-person space.</p>
<p><img src="/images/posts/raycasting/raycasteruntextured.gif" alt="Untextured raycaster result"></p>
<h2 id="textured-raycaster">Textured raycaster</h2>
<p>The textured version keeps the same raycasting core. The DDA walk does not change. The wall detection does not change. The perpendicular distance calculation does not change. The only difference is that walls are no longer filled with a flat color. Instead, each stripe samples a texture.</p>
<p>That means one more piece of information has to be calculated: the exact point where the ray hit the wall surface. The fractional part of that hit position tells which part of the wall was struck. From that, the x-coordinate inside the texture can be selected.</p>
<p>Once the x-coordinate is known, a second loop steps through the pixels of the stripe in the y direction. Each screen pixel maps to a corresponding texture y-coordinate. The texture sample is then copied into the screen buffer.</p>
<p><img src="/images/posts/raycasting/raycasttexture1.gif" alt="Textured raycaster world and walls"></p>
<p>The tutorial uses a screen buffer because drawing every pixel directly is usually too slow. Copying a full buffer to the screen is faster than issuing one draw call per pixel. That is a simple but important performance choice.</p>
<p>The textures can be generated procedurally for demonstration purposes. Some are gradients. Some are XOR patterns. Some are brick-like or solid-color variants. They are not fancy, but they are enough to show how the sampling works.</p>
<p><img src="/images/posts/raycasting/raycasttexture2.gif" alt="More textured walls"></p>
<p><img src="/images/posts/raycasting/raycasttexture3.gif" alt="Another textured view"></p>
<p>The y-side shading trick still applies. Since colors are packed into one integer, the darkening step uses a bit shift and a mask to reduce brightness without mixing the color channels together. The result is a tiny lighting cue that makes the surfaces easier to read.</p>
<p>The visual payoff is large. The same engine suddenly feels much richer because the walls now carry detail, pattern, and identity.</p>
<h2 id="wolfenstein-3d-textures">Wolfenstein 3D textures</h2>
<p>Instead of procedural patterns, the same renderer can load real wall textures. The tutorial shows textures from <em>Wolfenstein 3D</em>, which makes the example feel closer to the game that popularized the technique.</p>
<p><img src="/images/posts/raycasting/wolftextures.png" alt="Texture set from Wolfenstein 3D"></p>
<p>This part is pleasingly boring. The rendering logic stays the same. Only the source of the texture data changes. That is a good sign. It means the engine has a clean boundary between the raycasting math and the surface art.</p>
<p>The original game used separate light and dark versions of some walls. The tutorial takes a simpler route by using one texture per wall and darkening the y-side hits mathematically. That saves asset work and keeps the example compact.</p>
<p><img src="/images/posts/raycasting/wolftex1.gif" alt="Wolfenstein 3D style texture example"></p>
<p><img src="/images/posts/raycasting/wolftex2.gif" alt="Another Wolfenstein 3D style texture example"></p>
<h2 id="movement-collision-and-rotation">Movement, collision, and rotation</h2>
<p>A raycaster does not stop at drawing walls. It also needs movement. The sample uses a basic input loop with forward, backward, and rotation controls. The player position is updated by moving in the current direction vector, and collision is checked against the map before the position is accepted.</p>
<p>That collision check is simple, but it is enough for this kind of engine. The player is treated as a point. If the next position would land in a wall tile, the move is blocked. A more refined version could give the player a radius and check a small area around the position instead of just one point, but the point test keeps the tutorial easy to follow.</p>
<p>Rotation is handled by rotating both the direction vector and the camera plane. That matters because the direction vector alone is not enough. The plane has to stay in sync so the screen width continues to map correctly to the field of view.</p>
<p>The nice thing about this setup is that movement feels natural even though the underlying world is just a grid. The code does not need a 3D physics engine to let the player walk through corridors and turn at corners.</p>
<h2 id="performance-considerations">Performance considerations</h2>
<p>A software raycaster is simple, but simple does not mean free. Two performance issues stand out.</p>
<p>The first is memory locality. Raycasting naturally works on vertical stripes, while most screen buffers are laid out as horizontal scanlines. That means the write pattern is not especially cache-friendly. On a modern CPU, that mismatch can matter more than the ray math itself. A more advanced renderer could reorganize the memory layout or process data in a more cache-friendly order.</p>
<p>The second issue is software blitting. Copying the finished frame to the screen through a software path can become the bottleneck, especially at higher resolutions. Even if the raycasting loop is efficient, moving pixels around can still slow things down. Hardware rendering would help, but it changes the nature of the example.</p>
<p>Those concerns do not invalidate the technique. They just explain why a clean educational raycaster is not automatically the fastest thing on a modern machine. It is a good example of the difference between a useful algorithm and a production-tuned renderer.</p>
<h2 id="closing-thoughts">Closing thoughts</h2>
<p>Raycasting remains a great teaching example because it is small enough to understand and rich enough to matter. It shows how a 3D-looking world can be built from 2D data, how a camera can be modeled with vectors, and how a few careful shortcuts can turn a grid into a convincing first-person scene.</p>
<p>The untextured version teaches the geometry. The textured version adds depth and visual detail. The Wolfenstein textures tie the whole thing back to the history that made it famous. Together, they show that a lot of rendering is really about choosing the right simplification.</p>
<p>The big lesson is not that 3D graphics are fake. The lesson is that good rendering is often selective. It only computes what is needed for the image in front of the player, and it does so in a way that stays fast enough to run in real time.</p>
<p>That is why raycasting still feels clever after all these years.</p>
<h2 id="source">Source</h2>
<p>This post is a cleaned-up rewrite of Lode Vandevenne’s tutorial, <a href="https://lodev.org/cgtutor/raycasting.html">Raycasting</a>.</p>
]]></content>
        </item>
        
        <item>
            <title>Cancellation Tokens in Unity: Stopping Async Work Before It Outlives the Scene</title>
            <link>https://blog.jonmikeys.com/posts/cancellation-tokens-in-unity/</link>
            <pubDate>Thu, 23 Jul 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/cancellation-tokens-in-unity/</guid>
            <description>A practical follow-up to async in Unity: how cancellation tokens keep loading flows, delays, and background work from hanging around after the object is gone.</description>
            <content type="html"><![CDATA[<p>If the last post was about waiting in Unity without turning the main thread into a junk drawer, this one is about what happens when the wait should stop.</p>
<p>That is the part people skip until it hurts. A loading screen closes, a scene unloads, a menu gets backed out of, and some async flow keeps running anyway. A few seconds later, the code is still trying to touch an object that no longer exists. The bug usually shows up as a random exception, a stuck spinner, or a callback that arrives long after the player has moved on.</p>
<p>Cancellation tokens are the boring fix. They do not make async code faster, and they do not magically kill arbitrary work. They just give every step in the flow a shared signal that says, &ldquo;this no longer matters.&rdquo; That is exactly what Unity code needs when lifetime and timing are part of the problem.</p>
<p><img src="/images/posts/cancellation-tokens-in-unity/diagram.svg" alt="Cancellation Tokens In Unity diagram"></p>
<h2 id="what-a-cancellation-token-actually-does">What a cancellation token actually does</h2>
<p>A cancellation token is not a stop button in the dramatic sense. It is a lightweight signal.</p>
<p>Any method that accepts a <code>CancellationToken</code> can check whether cancellation has been requested and decide what to do next. That usually means one of three things:</p>
<ul>
<li>return early before doing more work</li>
<li>stop awaiting the next async step</li>
<li>throw <code>OperationCanceledException</code> so the caller can unwind cleanly</li>
</ul>
<p>That sounds small, but small is the point. Most Unity bugs around cancellation are not about one giant task. They are about several tiny steps that were written as if the object would always still be there.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>scene / object lifetime
</span></span><span style="display:flex;"><span>        │
</span></span><span style="display:flex;"><span>        ├── user leaves menu
</span></span><span style="display:flex;"><span>        ├── object gets destroyed
</span></span><span style="display:flex;"><span>        └── scene changes
</span></span><span style="display:flex;"><span>                 │
</span></span><span style="display:flex;"><span>                 ▼
</span></span><span style="display:flex;"><span>        cancellation token is signaled
</span></span><span style="display:flex;"><span>                 │
</span></span><span style="display:flex;"><span>                 ▼
</span></span><span style="display:flex;"><span>        async flow stops at the next check
</span></span></code></pre></div><h2 id="why-this-matters-in-unity">Why this matters in Unity</h2>
<p>Unity code lives inside lifetimes. A <code>MonoBehaviour</code> can be destroyed. A scene can unload. A UI panel can be hidden. A player can cancel a load screen and move somewhere else.</p>
<p>If the async work keeps going after that, you usually get one of these outcomes:</p>
<ul>
<li>stale UI updates</li>
<li>exceptions from dead objects</li>
<li>work that wastes time after the result no longer matters</li>
<li>a flow that is hard to reason about because the cause and effect are no longer attached</li>
</ul>
<p>That is why cancellation is not an optional cleanup feature. It is part of the control flow.</p>
<p>If the object that started the work is gone, the work should usually go with it.</p>
<h2 id="the-default-pattern-i-use">The default pattern I use</h2>
<p>I try to keep the rule simple: create or receive a token at the edge, then pass it all the way through.</p>
<p>In UniTask projects, I usually start from the object lifetime token:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Threading;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> Cysharp.Threading.Tasks;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ProfilePanel</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> GameObject loadingSpinner;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnEnable()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        RefreshAsync(<span style="color:#66d9ef">this</span>.GetCancellationTokenOnDestroy()).Forget();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">async</span> UniTask RefreshAsync(CancellationToken token)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        loadingSpinner.SetActive(<span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> profile = <span style="color:#66d9ef">await</span> LoadProfileAsync(token);
</span></span><span style="display:flex;"><span>            token.ThrowIfCancellationRequested();
</span></span><span style="display:flex;"><span>            ShowProfile(profile);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">catch</span> (OperationCanceledException)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Expected when the panel is closed or the scene changes.</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">finally</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">this</span> != <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                loadingSpinner.SetActive(<span style="color:#66d9ef">false</span>);
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">async</span> UniTask&lt;<span style="color:#66d9ef">string</span>&gt; LoadProfileAsync(CancellationToken token)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">await</span> UniTask.Delay(TimeSpan.FromSeconds(<span style="color:#ae81ff">1</span>), cancellationToken: token);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;Player One&#34;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> ShowProfile(<span style="color:#66d9ef">string</span> profile)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Update Unity objects here.</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The pattern is boring on purpose.</p>
<ul>
<li>the UI turns on before the wait starts</li>
<li>the token is threaded through the whole flow</li>
<li>cancellation is treated as normal control flow</li>
<li>cleanup happens in <code>finally</code></li>
</ul>
<p>That is enough for most game UI and loading code.</p>
<h2 id="cancellation-only-works-if-you-pass-the-token">Cancellation only works if you pass the token</h2>
<p>This is the part that gets missed most often.</p>
<p>A token only helps if the async method actually observes it. If you create the token and then forget to pass it into the awaited work, you have written a note to yourself and called it safety.</p>
<p>That means this is good:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">await</span> UniTask.Delay(<span style="color:#ae81ff">500</span>, cancellationToken: token);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">await</span> request.SendAsync(token);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">await</span> LoadSomethingAsync(token);
</span></span></code></pre></div><p>And this is not enough:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">await</span> UniTask.Delay(<span style="color:#ae81ff">500</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">await</span> request.SendAsync();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">await</span> LoadSomethingAsync();
</span></span></code></pre></div><p>If the API you are calling does not accept a token, cancellation can only happen between steps. That is still useful, but it is weaker than full propagation.</p>
<p>That is also why I like to make the token part of the method signature instead of hiding it in a field. If a method depends on cancellation, the signature should say so.</p>
<h2 id="user-cancel-and-lifetime-cancel-are-usually-different-things">User cancel and lifetime cancel are usually different things</h2>
<p>A UI flow often needs more than one reason to stop.</p>
<p>The object might disappear because the player left the screen. Or the player might press a cancel button while the object is still alive. Those are different sources of cancellation, but they should usually end up in the same flow.</p>
<p>That is where linked tokens help:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Threading;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> Cysharp.Threading.Tasks;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DownloadDialog</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> GameObject spinner;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> CancellationTokenSource? _userCancelSource;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnEnable()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _userCancelSource = <span style="color:#66d9ef">new</span> CancellationTokenSource();
</span></span><span style="display:flex;"><span>        RunAsync(_userCancelSource.Token, <span style="color:#66d9ef">this</span>.GetCancellationTokenOnDestroy()).Forget();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">async</span> UniTask RunAsync(CancellationToken userToken, CancellationToken destroyToken)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">using</span> var linked = CancellationTokenSource.CreateLinkedTokenSource(userToken, destroyToken);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> token = linked.Token;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        spinner.SetActive(<span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> UniTask.Delay(TimeSpan.FromSeconds(<span style="color:#ae81ff">3</span>), cancellationToken: token);
</span></span><span style="display:flex;"><span>            token.ThrowIfCancellationRequested();
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">catch</span> (OperationCanceledException)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Either the user canceled or the dialog went away.</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">finally</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">this</span> != <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                spinner.SetActive(<span style="color:#66d9ef">false</span>);
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> CancelPressed()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _userCancelSource?.Cancel();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnDisable()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _userCancelSource?.Dispose();
</span></span><span style="display:flex;"><span>        _userCancelSource = <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That pattern gives the flow two exit ramps:</p>
<ul>
<li>one for the player</li>
<li>one for the object lifetime</li>
</ul>
<p>The work stops when either one says it should.</p>
<h2 id="the-mistakes-i-try-to-avoid">The mistakes I try to avoid</h2>
<p>Cancellation is easy to get mostly right and still leave sharp edges behind.</p>
<p>The common mistakes are:</p>
<ul>
<li>swallowing cancellation and pretending the flow finished normally</li>
<li>creating a <code>CancellationTokenSource</code> and never disposing it</li>
<li>passing the token into some calls but not others</li>
<li>using cancellation as a substitute for proper error handling</li>
<li>assuming cancellation will interrupt work that never checks the token</li>
</ul>
<p>The last one matters most. Cancellation is cooperative. It works because your code checks the signal at sensible points.</p>
<p>That is not a weakness. It is what keeps cancellation predictable.</p>
<h2 id="a-simple-rule-of-thumb">A simple rule of thumb</h2>
<p>If an async flow belongs to an object, ask two questions:</p>
<ol>
<li>should this stop when the object dies?</li>
<li>should this also stop when the player cancels the action?</li>
</ol>
<p>If the answer to either one is yes, make the token part of the flow from the start.</p>
<p>If the answer is no, the work probably does not belong in that object in the first place.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Cancellation tokens do not make Unity async code clever. They make it honest.</p>
<p>They tell you which work still matters, keep stale results from leaking into the wrong scene, and make cancellation part of the design instead of an afterthought.</p>
<p>If the previous post was about waiting cleanly, this one is about stopping cleanly.</p>
<h2 id="references">References</h2>
<ul>
<li>Follow-up to <a href="https://blog.jonmikeys.com/posts/async-in-unity-with-unitask/">Async in Unity with UniTask: Waiting Without Turning the Main Thread into a Junk Drawer</a></li>
<li>Unity Scripting API, <a href="https://learn.microsoft.com/dotnet/api/system.threading.cancellationtokensource">CancellationTokenSource</a></li>
<li>Unity Manual, <a href="https://docs.unity3d.com/2023.2/Documentation/Manual/AwaitSupport.html">Await support</a></li>
<li>Cysharp, <a href="https://github.com/cysharp/UniTask">UniTask GitHub repository</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Mirror Multiplayer, Part 1: Authority First, RPCs Second</title>
            <link>https://blog.jonmikeys.com/posts/mirror-multiplayer-part-1-authority-first-rpcs-second/</link>
            <pubDate>Tue, 21 Jul 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/mirror-multiplayer-part-1-authority-first-rpcs-second/</guid>
            <description>A practical way to structure Mirror games so the server owns the rules, clients send intent, and state sync stays boring.</description>
            <content type="html"><![CDATA[<p>When a multiplayer feature starts in Mirror, RPCs should not be the first thing on the page.</p>
<p>RPCs are the part that looks exciting because they move across the network. They are also easy to reach for because they solve a visible problem fast: one machine says something happened, another machine reacts. That makes for a satisfying demo. It also makes for fragile architecture if RPCs end up carrying the rules, the state, and the consequences all at once.</p>
<p>A cleaner multiplayer flow starts with a more boring question: who owns the truth?</p>
<p>If that answer is unclear, the rest of the system starts to wobble. Input paths drift. UI becomes speculative. Cheating checks get scattered through unrelated scripts. Late-joining players miss important state. The codebase grows a ring of “temporary” network shortcuts that never really get removed.</p>
<p>Mirror works best when it is treated as a distribution layer, not as the place where game rules live.</p>
<p><img src="/images/posts/mirror-multiplayer-part-1-authority-first-rpcs-second/diagram.svg" alt="Mirror Multiplayer Part 1 Authority First Rpcs Second diagram"></p>
<h2 id="the-split-that-keeps-the-code-honest">The split that keeps the code honest</h2>
<p>A useful Mirror project usually separates networked behavior into three categories:</p>
<ul>
<li><strong>Commands</strong> for requests from an owned client to the server</li>
<li><strong>SyncVars or serialized state</strong> for persistent game state</li>
<li><strong>RPCs</strong> for short-lived effects, not for truth</li>
</ul>
<p>That split is not about style points. It is about keeping the direction of information clear.</p>
<p>The server owns the rules. The client sends intent. The rest of the clients observe the result. Once that separation is in place, the code becomes easier to reason about because each mechanism has one job instead of four.</p>
<p>A simple way to picture it is this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[Client / owned object]
</span></span><span style="display:flex;"><span>      input
</span></span><span style="display:flex;"><span>        |
</span></span><span style="display:flex;"><span>        v
</span></span><span style="display:flex;"><span>     Command
</span></span><span style="display:flex;"><span>        |
</span></span><span style="display:flex;"><span>        v
</span></span><span style="display:flex;"><span>[Server / rules + validation]
</span></span><span style="display:flex;"><span>   |                   \
</span></span><span style="display:flex;"><span>   |                    \-- RPCs: one-shot effects
</span></span><span style="display:flex;"><span>   v
</span></span><span style="display:flex;"><span>SyncVars / serialized state
</span></span><span style="display:flex;"><span>   |
</span></span><span style="display:flex;"><span>   v
</span></span><span style="display:flex;"><span>[Other clients / observers]
</span></span></code></pre></div><p>The arrows matter more than the boxes. Commands travel upward as requests. SyncVars travel outward as state. RPCs fan out as brief side effects.</p>
<h2 id="what-authority-means-in-practice">What authority means in practice</h2>
<p>Mirror uses words like ownership and authority in ways that can sound interchangeable at first glance. They are not.</p>
<p>Ownership describes routing. If a client owns an object, that client has a path for sending intent related to that object. Authority describes whether the object or component is allowed to be modified through that route. Mirror’s own comments around <code>SyncDirection</code>, <code>isOwned</code>, and authority make this distinction more precise than a lot of networking tutorials do.</p>
<p>That difference matters.</p>
<p>A local player may own a character controller. That does not mean the client gets to decide what the character’s health, currency, or inventory should be. Ownership says who can speak for the object. Authority says which changes are allowed to happen through that channel. The server still decides whether the request is valid and whether the state should actually change.</p>
<p>That is the mental model worth keeping nearby. Ownership routes messages. Authority gates changes. Neither one makes the client the source of truth.</p>
<h2 id="the-server-should-own-the-rules">The server should own the rules</h2>
<p>A multiplayer game gets brittle the moment the client starts deciding outcomes.</p>
<p>If the client can directly set position, health, inventory, or currency and then merely inform the server afterwards, the network is just a messenger for whatever the client chose to claim. The game may still appear to work in a local test, but the architecture has already lost the ability to defend itself.</p>
<p>A safer order is simple:</p>
<ol>
<li>the client sends intent</li>
<li>the server validates the request</li>
<li>the server mutates the authoritative state</li>
<li>the server syncs the result back out</li>
</ol>
<p>That sequence handles combat, pickups, doors, inventory changes, interactions, and most other gameplay systems that matter in a real networked game.</p>
<p>The key idea is that the request is not the outcome. A command is a request. A SyncVar or serialized state is the outcome. The code stays much more stable when those two are not mixed together.</p>
<h2 id="rpcs-are-for-moments-not-facts">RPCs are for moments, not facts</h2>
<p>RPCs are useful, but they are easy to misuse.</p>
<p>A common mistake is to use an RPC to carry state. That works until the edge cases arrive. Late-joining clients miss the message. Packet loss makes the event vanish. Reconnects break the illusion. A piece of information that should have persisted was treated like a one-time announcement.</p>
<p>That is the boundary that should stay in mind:</p>
<ul>
<li>use an RPC for a sound effect</li>
<li>use an RPC for a muzzle flash</li>
<li>use an RPC for a one-off animation cue</li>
<li>use an RPC for a temporary camera shake or burst of feedback</li>
</ul>
<p>If the information needs to survive time, it belongs in synchronized state.</p>
<p>A health value is state. A death effect is an event. A keycard being owned is state. A pickup flash is an event. Mixing those categories is one of the fastest ways to create confusing multiplayer code.</p>
<p>RPCs should answer the question “what effect should happen now?” They should not answer “what is the game world right now?”</p>
<h2 id="a-small-example-with-the-right-direction">A small example with the right direction</h2>
<p>A combat example makes the split easier to see.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> Mirror;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerCombat</span> : NetworkBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SyncVar(hook = nameof(OnHealthChanged))]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> health = <span style="color:#ae81ff">100</span>;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [Command]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> CmdRequestTakeDamage(<span style="color:#66d9ef">int</span> amount)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (amount &lt;= <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        health = Mathf.Max(health - amount, <span style="color:#ae81ff">0</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (health == <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>            RpcPlayDeathEffect();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [ClientRpc]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> RpcPlayDeathEffect()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// one-shot effect only</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnHealthChanged(<span style="color:#66d9ef">int</span> oldValue, <span style="color:#66d9ef">int</span> newValue)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// update UI here</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The syntax is not the interesting part.</p>
<p>The important part is the direction of control. The client does not set <code>health</code> directly. It asks the server to apply damage. The server decides whether that request is acceptable, mutates the authoritative value, and Mirror distributes the result. The UI hook updates from the state change rather than from a guessed local copy.</p>
<p>That gives three useful properties right away:</p>
<ul>
<li>invalid input can be rejected on the server</li>
<li>late joiners can still receive the current state</li>
<li>UI can stay tied to real data instead of duplicated logic</li>
</ul>
<p>That is a much safer foundation than trying to keep a few “helpful” local copies in sync by hand.</p>
<h2 id="commands-syncvars-and-rpcs-each-earn-their-place">Commands, SyncVars, and RPCs each earn their place</h2>
<p>Each mechanism solves a different part of the problem.</p>
<h3 id="commands">Commands</h3>
<p>Commands are best when a client owns the object and wants to request an action from the server. They are a good fit for player input, interaction requests, ability triggers, and other client-originated intentions.</p>
<h3 id="syncvars-and-serialized-state">SyncVars and serialized state</h3>
<p>SyncVars are the obvious choice for values that should exist no matter when a client arrives. Health, team, ready state, match phase, ownership flags, and a lot of UI-driving numbers belong here. If a player disconnects and reconnects, the state should still make sense.</p>
<h3 id="rpcs">RPCs</h3>
<p>RPCs are for brief side effects. They are the right choice when something should happen now, but does not need to persist as a rule of the world.</p>
<p>The practical payoff is that each network feature becomes easier to classify. When a new bit of multiplayer behavior appears, the question is not “how do these APIs work?” The question is “is this a request, persistent state, or a transient effect?” That is a much easier decision to make correctly.</p>
<h2 id="where-client-authority-still-makes-sense">Where client authority still makes sense</h2>
<p>Client authority is not the enemy. It is just easy to overtrust.</p>
<p>Mirror’s own direction-based comments make the point clearly: the important issue is not whether a client can speak for an object, but whether the flow of data still makes sense. A client-owned object can be a perfectly good place for local input, local prediction, or owner-only state as long as the server still has room to validate what comes back.</p>
<p>That usually fits cases like these:</p>
<ul>
<li>a local player controller</li>
<li>input-driven interaction probes</li>
<li>owner-only cosmetic state</li>
<li>private UI state tied to the local player</li>
</ul>
<p>It does not fit shared truth.</p>
<p>If two or more players care about the same outcome, the server should usually own that outcome. Otherwise the code is relying on trust where it should be relying on verification.</p>
<h2 id="the-pieces-that-usually-go-wrong">The pieces that usually go wrong</h2>
<p>The same mistakes show up in multiplayer code over and over again.</p>
<h3 id="mistake-1-the-client-writes-reality">Mistake 1: the client writes reality</h3>
<p>If the client changes the real state directly, the server becomes a witness instead of an authority. That is a bad sign even before cheating becomes a concern.</p>
<h3 id="mistake-2-rpcs-carry-persistent-state">Mistake 2: RPCs carry persistent state</h3>
<p>If a value needs to survive reconnects or late joins, it should not live in an RPC. A message can be missed. State should not.</p>
<h3 id="mistake-3-ui-is-treated-like-truth">Mistake 3: UI is treated like truth</h3>
<p>Local UI is allowed to predict or reflect state, but it should not become the source of truth. A health bar is not health. A label is not inventory. A menu is not the game.</p>
<h3 id="mistake-4-validation-is-deferred-until-later">Mistake 4: validation is deferred until “later”</h3>
<p>Later usually means never. The server should validate the request at the moment it receives it.</p>
<h3 id="mistake-5-authority-and-ownership-are-conflated">Mistake 5: authority and ownership are conflated</h3>
<p>This one causes a lot of confusion. A client can own an object without being allowed to define the game world. Routing is not trust.</p>
<h2 id="a-practical-checklist-for-mirror-code">A practical checklist for Mirror code</h2>
<p>When a new feature needs networking, the following checklist usually keeps the design on the rails:</p>
<ul>
<li>Does this value need to survive reconnects or late joins? Use state.</li>
<li>Is this a request from the owning client? Use a Command.</li>
<li>Is this only a momentary effect? Use an RPC.</li>
<li>Can the request be cheated? Validate it on the server.</li>
<li>Does the client merely observe the result? Keep it observing.</li>
</ul>
<p>If that checklist feels repetitive, good. Multiplayer code benefits from repetition because the same categories keep coming back.</p>
<h2 id="why-this-shape-ages-well">Why this shape ages well</h2>
<p>Mirror code that starts with authority tends to age better because it avoids making the client a parallel truth source.</p>
<p>That matters when a game grows. New features rarely arrive in isolation. A simple pickup becomes a scoreboard entry, a notification, a sound effect, and a persistence rule. A movement system becomes animation, interpolation, spectator logic, and anti-cheat checks. When the core direction of state is already clear, those additions stay manageable.</p>
<p>The opposite pattern is expensive. If the game starts with RPCs everywhere, the codebase eventually spends more time explaining itself than shipping features. The network layer becomes a web of special cases. Debugging becomes archaeology.</p>
<p>Authority-first architecture is not glamorous. It is just the shape that keeps multiplayer logic boring enough to survive real production work.</p>
<h2 id="part-1-ends-with-a-simple-rule">Part 1 ends with a simple rule</h2>
<p>Treat Mirror as a way to distribute already-decided game truth.</p>
<p>The client should request. The server should decide. The state should sync. The RPC should decorate, not define. That one habit prevents a surprising amount of pain later.</p>
<h2 id="next-part">Next part</h2>
<p>This is Part 1.</p>
<p>Part 2 can stay close to the same structure and push it into a concrete player-action loop: input, validation, sync, owner-only UI, and the point where an RPC is still the right tool instead of a temptation.</p>
<h2 id="references">References</h2>
<ul>
<li>Mirror README, feature list and project overview: <a href="https://raw.githubusercontent.com/MirrorNetworking/Mirror/master/README.md">https://raw.githubusercontent.com/MirrorNetworking/Mirror/master/README.md</a></li>
<li>Mirror source, <code>NetworkBehaviour.cs</code> comments on <code>SyncDirection</code>, <code>isOwned</code>, and <code>authority</code>: <a href="https://raw.githubusercontent.com/MirrorNetworking/Mirror/master/Assets/Mirror/Core/NetworkBehaviour.cs">https://raw.githubusercontent.com/MirrorNetworking/Mirror/master/Assets/Mirror/Core/NetworkBehaviour.cs</a></li>
<li>Mirror example, <code>Assets/Mirror/Examples/AssignAuthority/Scripts/OwnerBehaviour.cs</code>: <a href="https://raw.githubusercontent.com/MirrorNetworking/Mirror/master/Assets/Mirror/Examples/AssignAuthority/Scripts/OwnerBehaviour.cs">https://raw.githubusercontent.com/MirrorNetworking/Mirror/master/Assets/Mirror/Examples/AssignAuthority/Scripts/OwnerBehaviour.cs</a></li>
<li>Mirror example, <code>Assets/Mirror/Examples/AssignAuthority/Scripts/Player.cs</code>: <a href="https://raw.githubusercontent.com/MirrorNetworking/Mirror/master/Assets/Mirror/Examples/AssignAuthority/Scripts/Player.cs">https://raw.githubusercontent.com/MirrorNetworking/Mirror/master/Assets/Mirror/Examples/AssignAuthority/Scripts/Player.cs</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Async in Unity with UniTask: Waiting Without Turning the Main Thread into a Junk Drawer</title>
            <link>https://blog.jonmikeys.com/posts/async-in-unity-with-unitask/</link>
            <pubDate>Fri, 17 Jul 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/async-in-unity-with-unitask/</guid>
            <description>A practical way to use async and await in Unity without falling back to callback soup, extra GC churn, or tangled loading code.</description>
            <content type="html"><![CDATA[<p>Async code in Unity is useful when something needs to wait without freezing the rest of the game.</p>
<p>That sounds straightforward until a project grows beyond a small prototype. Then the waiting logic starts to sprawl across coroutines, callbacks, manager classes, helper methods, and extra <code>Update</code> checks that exist only to ask whether a task is done yet. The result usually works, but it becomes hard to read, hard to cancel, and hard to reason about when something goes wrong.</p>
<p>UniTask gives that problem a cleaner shape. It keeps the <code>async</code> and <code>await</code> style, but stays close to Unity’s execution model and avoids the allocation habits that make plain <code>Task</code> awkward in hot gameplay code.</p>
<p><img src="/images/posts/async-in-unity-with-unitask/diagram.svg" alt="Async In Unity With Unitask diagram"></p>
<h2 id="what-async-in-unity-actually-needs-to-solve">What async in Unity actually needs to solve</h2>
<p>Most Unity projects do not need async because async is fashionable. They need it because some work takes time.</p>
<p>The real problem is not waiting. The real problem is waiting in a way that does not wreck the frame loop, does not lose the thread boundary, and does not turn the code into callback soup.</p>
<p>A useful async setup in Unity usually needs three things:</p>
<ul>
<li>code that reads in order</li>
<li>a way to wait without blocking the main thread</li>
<li>a way to keep garbage collection pressure under control</li>
</ul>
<p>That is the core of it.</p>
<p>Anything else is decoration. If a flow touches Unity objects, it still has to respect Unity’s thread rules. Async changes how the code is written, but it does not change where engine APIs are safe to call. Main-thread-only work still belongs on the main thread.</p>
<h2 id="the-mental-model-that-keeps-async-sane">The mental model that keeps async sane</h2>
<p>A good way to think about Unity async is as a route through two lanes.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        main thread / PlayerLoop
</span></span><span style="display:flex;"><span>caller ───────► awaitable work ───────► continuation
</span></span><span style="display:flex;"><span>   │                 │                      │
</span></span><span style="display:flex;"><span>   │                 │                      └─ apply results to Unity objects
</span></span><span style="display:flex;"><span>   │                 └─ background thread for pure C# work only
</span></span><span style="display:flex;"><span>   └─ start loading / input / UI flow
</span></span></code></pre></div><p>That diagram is boring on purpose. The boring part is the part that keeps the game from breaking.</p>
<p>The mistake is not using async. The mistake is pretending every step in the flow is safe to run in every context.</p>
<p>If a save file is being parsed, that part can often move to a background thread because it is pure C# work. If a <code>Transform</code>, <code>SceneManager</code>, UI widget, or most engine APIs are involved, that part needs to happen on the main thread. The boundary matters more than the syntax.</p>
<h2 id="why-unitask-fits-unity-better-than-plain-task-in-game-code">Why UniTask fits Unity better than plain Task in game code</h2>
<p>Plain <code>Task</code> is fine in general C# code. In Unity, it can become awkward in the places where the game repeats small waits all the time.</p>
<p>Scene transitions. Loading screens. Timed UI sequences. Network polls. Animation-adjacent setup steps. Asset preparation. These are the sorts of flows that appear everywhere in a game project, and they tend to be short-lived, frequent, and sensitive to allocations.</p>
<p>UniTask is built with Unity’s PlayerLoop in mind. It gives awaitable delays, frame waits, Unity async operations, cancellation helpers, and a lightweight model that is better aligned with gameplay code than the generic <code>Task</code> world.</p>
<p>Coroutines can still be a fine tool. They are not bad. They are just a different shape. When a flow is tiny and frame-oriented, an <code>IEnumerator</code> can still be the simplest thing that works. But once a feature starts mixing waits, cancellation, error handling, and async requests, a real async method usually reads better and scales better.</p>
<p>That is the useful distinction: coroutines are good at frame choreography; UniTask is good at procedural async flows that still need to live inside Unity.</p>
<h2 id="the-main-thing-to-keep-straight-unity-objects-are-not-thread-safe">The main thing to keep straight: Unity objects are not thread-safe</h2>
<p>The biggest source of bugs in async Unity code is thread confusion.</p>
<p>If code is touching engine objects, it belongs on the main thread. If code is doing pure computation or file work, that part can often move off-thread. Those are not the same thing, and treating them as if they are interchangeable causes very strange bugs.</p>
<p>The rule is simple enough to remember:</p>
<ul>
<li>pure C# work can often move to the thread pool</li>
<li>Unity object access should come back to the main thread</li>
</ul>
<p>That boundary is especially important during loading flows. Parsing JSON or deserializing a save file can often happen off-thread. Applying the results to a scene, a UI, or a player object should happen on the main thread.</p>
<h2 id="a-practical-loading-flow">A practical loading flow</h2>
<p>A loading or save-import flow is one of the cleanest places to use UniTask.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.IO;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Threading;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> Cysharp.Threading.Tasks;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> UnityEngine;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SaveLoader</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> GameObject loadingSpinner;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">string</span> savePath;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> Start()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        LoadSaveAsync(<span style="color:#66d9ef">this</span>.GetCancellationTokenOnDestroy()).Forget();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">async</span> UniTask LoadSaveAsync(CancellationToken token)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        loadingSpinner.SetActive(<span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> UniTask.SwitchToThreadPool();
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> json = File.ReadAllText(savePath);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> saveData = ParseSaveData(json);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> UniTask.SwitchToMainThread(token);
</span></span><span style="display:flex;"><span>            ApplySave(saveData);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">catch</span> (OperationCanceledException)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// The object was destroyed or the flow was cancelled.</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">finally</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">this</span> != <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                loadingSpinner.SetActive(<span style="color:#66d9ef">false</span>);
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> SaveData ParseSaveData(<span style="color:#66d9ef">string</span> json)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Replace with the project serializer.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> SaveData();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> ApplySave(SaveData saveData)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Unity object updates stay here.</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [Serializable]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SaveData</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> level;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">string</span> playerName;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The important part is not the exact API names. The important part is the boundary.</p>
<p>The loading spinner is shown on the main thread. File reading and parsing move away from the main thread. The results come back to the main thread before touching Unity objects. That split is what keeps the code predictable.</p>
<h2 id="cancellation-is-not-optional">Cancellation is not optional</h2>
<p>Async code without cancellation is one of the fastest ways to create edge-case bugs.</p>
<p>If a scene changes, a menu closes, or a <code>MonoBehaviour</code> is destroyed while the async flow is still running, the work should stop. Otherwise the code may try to update dead objects, close a loading screen that no longer exists, or fire an exception in a path that only appears under awkward user timing.</p>
<p>For Unity objects, a destroy token is the natural fit:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> token = <span style="color:#66d9ef">this</span>.GetCancellationTokenOnDestroy();
</span></span></code></pre></div><p>Passing that token through the whole flow makes the lifetime explicit. The task then belongs to the object that started it, not to some vague global background process.</p>
<p>That one habit prevents a lot of “why did this callback fire after the scene was gone?” debugging sessions.</p>
<h2 id="where-unitask-is-a-better-fit-than-coroutines">Where UniTask is a better fit than coroutines</h2>
<p>UniTask usually wins when a flow has one or more of these traits:</p>
<ul>
<li>it needs cancellation</li>
<li>it chains several async steps together</li>
<li>it mixes Unity async operations with pure C# work</li>
<li>it needs <code>WhenAll</code> or parallel waits</li>
<li>it reads more clearly as procedural code than as an iterator</li>
</ul>
<p>Coroutines still make sense when the flow is tiny and frame-oriented.</p>
<p>A simple fade animation, a short delay, or a legacy system that already speaks <code>IEnumerator</code> does not need a dramatic rewrite. Using async everywhere just because it exists is a good way to add ceremony without getting any of the benefits.</p>
<p>The smaller tool is usually the correct one.</p>
<h2 id="fire-and-forget-has-a-real-cost">Fire-and-forget has a real cost</h2>
<p>A lot of Unity async code becomes messy when it hides errors.</p>
<p>Calling <code>Forget()</code> can be a reasonable choice when the task is truly background work that should start and move on. It is not a license to ignore failure paths. Any fire-and-forget flow still needs a clear way to surface exceptions, handle cancellation, and avoid leaving the game in a half-updated state.</p>
<p>That is where many teams accidentally make async harder than coroutines. The method signature looks clean, but the failure mode is now disconnected from the caller. If that path fails silently, the game may keep running with a broken loading screen or a half-applied state change.</p>
<p>The rule is simple: if the flow matters, the failure path matters too.</p>
<h2 id="what-usually-goes-wrong">What usually goes wrong</h2>
<p>A few mistakes show up again and again in Unity async code.</p>
<h3 id="mistake-1-touching-unity-objects-off-thread">Mistake 1: touching Unity objects off-thread</h3>
<p>This one causes the strangest bugs because it sometimes appears to work until the timing changes.</p>
<h3 id="mistake-2-forgetting-cancellation">Mistake 2: forgetting cancellation</h3>
<p>Async work outliving its object is a classic source of orphaned UI, exceptions, and weird late callbacks.</p>
<h3 id="mistake-3-using-async-for-synchronous-work">Mistake 3: using async for synchronous work</h3>
<p>If the work is small and immediate, async can add more complexity than it removes.</p>
<h3 id="mistake-4-letting-callback-chains-grow-unchecked">Mistake 4: letting callback chains grow unchecked</h3>
<p>Async exists partly to prevent callback soup. A project that just re-creates the same soup with different syntax has not improved anything.</p>
<h3 id="mistake-5-using-background-threads-as-a-shortcut">Mistake 5: using background threads as a shortcut</h3>
<p>Only pure C# work belongs there. Anything that depends on Unity state should stay on the main thread.</p>
<h2 id="unitask-coroutines-and-unitys-built-in-await-support">UniTask, coroutines, and Unity’s built-in await support</h2>
<p>Unity’s async story has improved a lot over time. Coroutines remain useful. Built-in await support has become better. That is good news because it reduces the old need to invent a custom flow for every async case.</p>
<p>UniTask still has a place because it is designed around the kinds of short, frequent waits that game code uses all the time. It fits nicely when a project wants a mature async layer with a lot of small utilities around the core model.</p>
<p>The easiest way to compare them is like this:</p>
<ul>
<li><strong>Coroutines</strong> are good for frame choreography and simple waits</li>
<li><strong>UniTask</strong> is good for structured async flows in game code</li>
<li><strong>Built-in await support</strong> makes async more viable inside the engine itself</li>
</ul>
<p>Those are overlapping tools, not enemies.</p>
<h2 id="a-practical-rule-of-thumb">A practical rule of thumb</h2>
<p>A quick checklist usually makes the decision obvious:</p>
<ul>
<li>does this flow need to wait?</li>
<li>does it need to be cancelled when an object dies?</li>
<li>does any part of it belong off the main thread?</li>
<li>would a callback chain make this harder to read?</li>
</ul>
<p>If the answer is yes to two or more, async is probably worth it.</p>
<p>If the code is just a tiny frame delay, a coroutine or even plain synchronous code may still be the smaller choice.</p>
<p>That is the real test. Use the smallest thing that solves the actual problem.</p>
<h2 id="a-slightly-richer-example-loading-a-scene-setup-flow">A slightly richer example: loading a scene setup flow</h2>
<p>A scene setup flow often combines several kinds of work:</p>
<ul>
<li>wait for a network or file request</li>
<li>parse data</li>
<li>update UI</li>
<li>spawn or configure Unity objects</li>
<li>hide loading feedback</li>
</ul>
<p>That is exactly the kind of sequence where async tends to read well.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">async</span> UniTask SetupSceneAsync(CancellationToken token)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    loadingOverlay.Show();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">await</span> LoadRemoteProfileAsync(token);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> data = <span style="color:#66d9ef">await</span> ReadLocalSettingsAsync(token);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">await</span> UniTask.SwitchToMainThread(token);
</span></span><span style="display:flex;"><span>        ApplySettings(data);
</span></span><span style="display:flex;"><span>        SpawnPlayer();
</span></span><span style="display:flex;"><span>        RefreshHUD();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">finally</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">this</span> != <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>            loadingOverlay.Hide();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That code reads top to bottom. The important branch points are visible. The thread boundary is explicit. Cancellation follows the lifetime of the object. This is the shape that makes async valuable in a real game project.</p>
<h2 id="the-practical-takeaway">The practical takeaway</h2>
<p>UniTask is most useful when async needs to stay readable, cancellable, and friendly to Unity’s main-thread rules.</p>
<p>A good Unity async flow usually follows a simple pattern:</p>
<ul>
<li>keep Unity object access on the main thread</li>
<li>move pure C# work off-thread when it helps</li>
<li>pass a destroy token through the flow</li>
<li>use coroutines only when they are actually the smaller tool</li>
</ul>
<p>That keeps waiting code from turning into another junk drawer.</p>
<h2 id="references">References</h2>
<ul>
<li>Unity Manual, <a href="https://docs.unity3d.com/6000.5/Documentation/Manual/Coroutines.html">Write and run coroutines</a></li>
<li>Unity Manual, <a href="https://docs.unity3d.com/2023.2/Documentation/Manual/AwaitSupport.html">Await support</a></li>
<li>Unity Manual, <a href="https://docs.unity3d.com/6000.2/Documentation/Manual/async-awaitable-continuations.html">Awaitable completion and continuation</a></li>
<li>Cysharp, <a href="https://github.com/cysharp/UniTask">UniTask GitHub repository</a></li>
<li>Unity Scripting API, <a href="https://docs.unity3d.com/6000.1/Documentation/ScriptReference/MonoBehaviour-destroyCancellationToken.html">MonoBehaviour.destroyCancellationToken</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Benchmarking C# in Unity Without Lying to Yourself</title>
            <link>https://blog.jonmikeys.com/posts/benchmarking-csharp-in-unity/</link>
            <pubDate>Mon, 13 Jul 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/benchmarking-csharp-in-unity/</guid>
            <description>A practical way to benchmark Unity C# code without confusing profiling, frame time, and microbenchmarks.</description>
            <content type="html"><![CDATA[<p>Benchmarking code in Unity is not about finding one fps number and declaring victory.</p>
<p>The real goal is narrower and more useful: did this change make the game faster, slower, or merely different? That question is easy to ask and surprisingly hard to answer honestly because game performance work mixes several separate activities and treats them as if they were one thing.</p>
<p>Profiling a live scene. Timing a small code path. Comparing two builds over time. Tracking regressions across a branch. Those jobs are related, but they are not interchangeable.</p>
<p>A benchmark only becomes useful when the question is precise and the test conditions are stable.</p>
<p><img src="/images/posts/benchmarking-csharp-in-unity/diagram.svg" alt="Benchmarking Csharp In Unity diagram"></p>
<h2 id="start-with-the-question-not-the-tool">Start with the question, not the tool</h2>
<p>The fastest way to get a bad benchmark is to start by opening a timer and hope the numbers explain themselves.</p>
<p>The better order is to decide what problem is actually being solved.</p>
<p>If a scene stutters, the Unity Profiler is the first stop.
If two versions of a function need comparing, a benchmark is the right tool.
If regressions need tracking over time, the benchmark should rerun in the same conditions again and again.</p>
<p>Unity’s own profiling guidance makes this point directly: profile before and after a change, use frame time instead of fps, and measure on the target device when the numbers matter. That advice sounds obvious, but it is the part people tend to skip when they are trying to move fast. Then the wrong thing gets optimized and the hitch remains.</p>
<h2 id="frame-time-beats-fps">Frame time beats fps</h2>
<p>Fps is fine for a headline. Frame time is what the player actually feels.</p>
<p>A game can report a comfortable average fps and still hitch badly if one frame blows the budget. That is why the useful questions are usually:</p>
<ul>
<li>how long does the average frame take?</li>
<li>what do the worst frames look like?</li>
<li>does the change shift the distribution or just the average?</li>
</ul>
<p>For a 60 fps target, the budget is about 16.6 ms per frame. That sounds like a lot until rendering, physics, animation, UI, scripting, and asset work all start competing for the same slice of time.</p>
<p>Average fps alone can hide spikes. A benchmark that ignores spikes is often lying by omission.</p>
<h2 id="profiling-microbenchmarking-and-regression-tracking-are-different-jobs">Profiling, microbenchmarking, and regression tracking are different jobs</h2>
<p>A profiler and a benchmark answer different questions.</p>
<p>A profiler shows where time went inside a real scene.
A microbenchmark shows whether a tiny piece of code changed in a useful way.
A regression test shows whether a known behavior stayed within a target range across builds.</p>
<p>Those are not the same because the context changes. The profiler includes engine overhead, scene setup, unrelated systems, asset work, and sometimes editor noise. That is valuable context when hunting a bottleneck. It is also why profiler numbers should not be treated as a perfect replacement for a focused benchmark when comparing two algorithms.</p>
<p>A benchmark should be boring:</p>
<ul>
<li>same input</li>
<li>same scene or fixture</li>
<li>same build configuration</li>
<li>same hardware</li>
<li>same warmup pattern</li>
<li>same number of repeated runs</li>
</ul>
<p>If those change, the test stops being a benchmark and turns into a story.</p>
<h2 id="warm-up-first">Warm up first</h2>
<p>The first run is often weird.</p>
<p>In the Editor, managed code may pay JIT or initialization costs. In a player build, the first run can still include lazy initialization, cache misses, asset loading, or one-time setup. The first sample is usually not the sample that should be trusted.</p>
<p>A simple benchmark usually does three things:</p>
<ol>
<li>warm up the code</li>
<li>discard the first run</li>
<li>measure several later runs and compare the median or average</li>
</ol>
<p>That is not glamorous. It just avoids the most obvious lie.</p>
<p>If a benchmark gets dramatically better or worse after the first sample, that is a sign the setup is still too noisy.</p>
<h2 id="use-the-right-timing-tool">Use the right timing tool</h2>
<p>For small C# timings, <code>Stopwatch</code> is the standard tool.</p>
<p>It exists for elapsed-time measurement, and the .NET docs make that explicit. That makes it far more appropriate than trying to infer microseconds from frame counters or from <code>Time.time</code>.</p>
<p>A tiny local benchmark can look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Diagnostics;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">static</span> <span style="color:#66d9ef">double</span> Benchmark(Action action, <span style="color:#66d9ef">int</span> iterations)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> sw = Stopwatch.StartNew();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i = <span style="color:#ae81ff">0</span>; i &lt; iterations; i++)
</span></span><span style="display:flex;"><span>        action();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    sw.Stop();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> sw.Elapsed.TotalMilliseconds;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That function only tells the truth if the work inside <code>action</code> is stable. If the action allocates, touches random data, depends on scene state, or mixes in unrelated work, the result is mostly noise.</p>
<p>So <code>Stopwatch</code> is useful for timing a specific code path, not for replacing a performance strategy.</p>
<h2 id="allocations-matter-as-much-as-time">Allocations matter as much as time</h2>
<p>In Unity, a faster path is not automatically a better path.</p>
<p>A change can shave time off a loop and still make the game worse if it starts allocating every frame. The Unity manual on GC tracking is blunt about this: <code>GC.Alloc</code> reports managed heap allocations, and editor measurements can be misleading. Accurate results come from profiling a development build on the target platform.</p>
<p>That means every benchmark should watch for two separate failure modes:</p>
<ul>
<li>the code got slower</li>
<li>the code got cheaper to run but started allocating more often</li>
</ul>
<p>The second one is sneakier because it often survives simple local checks. A function can look excellent in a microbenchmark and still cause garbage collection pain in a real scene.</p>
<p>If runtime counters are needed, <code>ProfilerRecorder</code> is useful because it can read profiler metrics from editor or player builds. The API is explicitly meant for this sort of observation. It still needs clean test conditions and careful disposal, though. A counter is only as useful as the test around it.</p>
<h2 id="editor-numbers-are-a-clue-not-the-answer">Editor numbers are a clue, not the answer</h2>
<p>The Editor is convenient. It is not the same as a player build.</p>
<p>Unity’s profiling guidance says this plainly: Play mode profiling is useful for quick checks, but target-platform profiling is where the most trustworthy numbers live. That is not an academic distinction. The Editor adds enough overhead that it can hide or invent problems.</p>
<p>When the result matters, the checklist usually looks like this:</p>
<ol>
<li>build a Development Build</li>
<li>run on the target device or platform</li>
<li>keep the scene and inputs fixed</li>
<li>warm up once</li>
<li>measure several times</li>
<li>compare against the baseline</li>
</ol>
<p>If a regression matters, do not trust a one-off Editor run. The Editor can tell a story, but the player build tells the truth.</p>
<h2 id="a-practical-benchmark-workflow">A practical benchmark workflow</h2>
<p>A useful benchmark workflow is small and repeatable.</p>
<h3 id="1-measure-the-baseline">1. Measure the baseline</h3>
<p>Record the current behavior before changing anything. A benchmark without a baseline is just a number.</p>
<h3 id="2-warm-up-the-code">2. Warm up the code</h3>
<p>Throw away the first run so startup noise does not contaminate the result.</p>
<h3 id="3-measure-the-same-thing-many-times">3. Measure the same thing many times</h3>
<p>Single samples are too easy to distort.</p>
<h3 id="4-watch-allocations-and-frame-time">4. Watch allocations and frame time</h3>
<p>A faster result is not better if GC pressure gets worse or frame spikes become more frequent.</p>
<h3 id="5-run-on-the-target-build">5. Run on the target build</h3>
<p>Editor timings are only a hint.</p>
<h3 id="6-change-one-thing-at-a-time">6. Change one thing at a time</h3>
<p>If five things change, the data stops explaining which change mattered.</p>
<p>That last step is where a lot of performance work goes wrong. People compare two versions that differ in half a dozen ways and then wonder why the result is muddy.</p>
<h2 id="what-not-to-benchmark">What not to benchmark</h2>
<p>Some things are bad benchmark candidates from the start.</p>
<p>Avoid benchmarking code that is:</p>
<ul>
<li>one-off setup that runs only at load</li>
<li>heavily dependent on random gameplay state</li>
<li>tied too closely to the rest of the scene</li>
<li>dominated by editor overhead instead of the code being measured</li>
</ul>
<p>Sometimes the right answer is not “write a benchmark.” Sometimes the right answer is “profile the real scene in a real build and stop pretending a microbenchmark will solve the problem.”</p>
<p>That is not a failure. That is choosing the right tool.</p>
<p>A benchmark is best when the code path is stable and isolated. A profiler is best when the code path is tangled with the rest of the game.</p>
<h2 id="why-small-changes-can-produce-noisy-results">Why small changes can produce noisy results</h2>
<p>Performance work gets messy because game code rarely lives alone.</p>
<p>A “simple” function may be affected by JIT warmup, collection pressure, scene layout, cache state, animation timing, or the order in which previous systems ran. In Unity, even a tiny code path can be influenced by surrounding engine behavior.</p>
<p>That is why benchmark results should be interpreted as ranges, not magical truth pills.</p>
<p>If a change improves a function by 2 percent in one run and hurts it by 1 percent in another, the result may simply be noise. The benchmark needs either more runs, a better fixture, or a larger signal. A small result can still matter, but only if the test is clean enough to trust.</p>
<h2 id="a-code-path-benchmark-can-stay-simple">A code path benchmark can stay simple</h2>
<p>A benchmark harness does not need to be fancy.</p>
<p>For many C# cases, a timer loop around a pure function is enough. What matters is the setup around it.</p>
<ul>
<li>initialize once</li>
<li>use fixed data</li>
<li>separate warmup from measurement</li>
<li>repeat several times</li>
<li>record the median if outliers are common</li>
</ul>
<p>That is usually enough to answer whether a change is worthwhile.</p>
<p>If the code depends on the scene, the benchmark fixture should look like the scene. If the code is pure computation, keep it pure. The more the benchmark resembles the real use case, the more useful the number becomes.</p>
<h2 id="the-practical-split-that-keeps-teams-sane">The practical split that keeps teams sane</h2>
<p>A useful performance workflow usually breaks into three tools:</p>
<ul>
<li><strong>Stopwatch</strong> for quick local timing</li>
<li><strong>Profiler</strong> for scene-level diagnosis</li>
<li><strong>Performance tests</strong> for repeatable comparisons over time</li>
</ul>
<p>That split keeps the team from asking one tool to do all three jobs.</p>
<p>The profiler is the camera. The benchmark is the stopwatch. The performance test is the regression guard.</p>
<h2 id="when-to-use-unity-performance-testing">When to use Unity performance testing</h2>
<p>If a repeatable benchmark needs to run as part of an automated test flow, Unity’s performance testing package is the right fit.</p>
<p>That is especially useful when comparing implementations and wanting the result to survive beyond a single laptop. The package is designed to collect metrics and compare results, which is exactly what regression tracking needs.</p>
<p>It is not necessary for every experiment. For a quick local check, <code>Stopwatch</code> is enough. For a benchmark that needs to be rerun tomorrow, a harness is better.</p>
<h2 id="a-useful-checklist-before-trusting-a-result">A useful checklist before trusting a result</h2>
<p>Before a benchmark result gets treated as real, check the following:</p>
<ul>
<li>was the code warmed up?</li>
<li>was the first run discarded?</li>
<li>did the test use fixed input?</li>
<li>was the build a Development Build?</li>
<li>was the test run on the target platform?</li>
<li>were allocations measured as well as time?</li>
<li>were the two versions changed in only one meaningful way?</li>
</ul>
<p>If any of those answers is no, the result should be treated as provisional.</p>
<h2 id="a-good-benchmark-does-not-need-drama">A good benchmark does not need drama</h2>
<p>The best benchmark is usually boring.</p>
<p>It uses the same inputs every time. It measures the same code path every time. It reports numbers that can be compared later. It does not try to prove too much. It just tells the truth about a narrow question.</p>
<p>That is the goal: a benchmark that helps decisions instead of producing confidence theater.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>For honest performance numbers in Unity, keep three rules nearby:</p>
<ul>
<li>profile the scene when bottlenecks matter</li>
<li>benchmark small code paths when comparisons matter</li>
<li>measure in a Development Build on the target platform when truth matters</li>
</ul>
<p>That keeps the project from chasing fake wins and helps catch real regressions before they ship.</p>
<h2 id="references">References</h2>
<ul>
<li>Unity Blog, <a href="https://unity.com/blog/engine-platform/performance-benchmarking-in-unity-how-to-get-started">Performance benchmarking in Unity: How to get started</a></li>
<li>Unity Manual, <a href="https://unity.com/how-to/best-practices-for-profiling-game-performance">Best practices for profiling game performance</a></li>
<li>Unity Manual, <a href="https://docs.unity3d.com/2020.1/Documentation/Manual/profiler-profiling-applications.html">Profiling your application</a></li>
<li>Unity Manual, <a href="https://docs.unity3d.com/6000.1/Documentation/Manual/performance-track-garbage-collection.html">Tracking garbage collection allocations</a></li>
<li>Unity Scripting API, <a href="https://docs.unity3d.com/6000.2/Documentation/ScriptReference/Unity.Profiling.ProfilerRecorder.html">ProfilerRecorder</a></li>
<li>Microsoft Learn, <a href="https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.stopwatch?view=net-10.0"><code>Stopwatch</code> Class</a></li>
<li>Unity Performance Testing Package, <a href="https://docs.unity3d.com/Packages/com.unity.test-framework.performance@latest/">Package documentation</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Building Game UI Systems That Stay Modular</title>
            <link>https://blog.jonmikeys.com/posts/modular-game-ui-systems/</link>
            <pubDate>Thu, 09 Jul 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/modular-game-ui-systems/</guid>
            <description>A practical UI structure for games: keep screens, views, and game logic separate so menus do not turn into a knot.</description>
            <content type="html"><![CDATA[<p>Game UI has a habit of growing sideways.</p>
<p>A pause menu becomes save/load, controller prompts, an options screen, notifications, a store, a battle HUD, and suddenly one class knows about every button in the game and half the gameplay systems too. The layer that should have been simple now contains navigation, business rules, scene lookup, and a pile of one-off exceptions.</p>
<p>That is the point where UI stops being a layer and turns into a junk drawer.</p>
<p>The fix is not cleverness. The fix is to stop letting UI objects do three jobs at once.</p>
<p><img src="/images/posts/modular-game-ui-systems/diagram.svg" alt="Modular Game Ui Systems diagram"></p>
<h2 id="the-shape-worth-aiming-for">The shape worth aiming for</h2>
<p>A modular game UI usually keeps four concerns separate:</p>
<ul>
<li><strong>flow control</strong> decides which screen is active</li>
<li><strong>presentation logic</strong> decides what the UI should show and how it reacts</li>
<li><strong>views</strong> draw the widgets and raise user events</li>
<li><strong>services and game state</strong> live outside the UI layer</li>
</ul>
<p>That split is not academic. It keeps the UI from becoming the place where navigation, business rules, and scene lookup all meet and start fighting.</p>
<p>Unity’s MVVM documentation makes the same separation in different words: the view is for display, the viewmodel contains the logic, and the model is the data. The Game Developer article on Unity UI architecture makes a similar point with screens, layers, and widgets: compose the system, keep the rest of the game agnostic, and do not let random code reach into the internals.</p>
<p>That is the model worth copying.</p>
<h2 id="keep-the-view-dumb">Keep the view dumb</h2>
<p>The view should not decide whether the game is paused, whether the player can afford an item, or whether a mission is complete.</p>
<p>Its job is narrower:</p>
<ul>
<li>show state</li>
<li>raise intent</li>
<li>stay out of the rules</li>
</ul>
<p>A bad UI script usually starts by being convenient and ends by owning everything.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PauseButton</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> OnClickResume()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Time.timeScale = <span style="color:#ae81ff">1f</span>;
</span></span><span style="display:flex;"><span>        AudioListener.pause = <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>        GameManager.Instance.SetPaused(<span style="color:#66d9ef">false</span>);
</span></span><span style="display:flex;"><span>        FindObjectOfType&lt;MenuStack&gt;().CloseAll();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is easy to write and hard to maintain.</p>
<p>The button knows about time, audio, game flow, and menu management. If any one of those systems changes, the button has to be remembered too. The button is now part of the architecture instead of part of the presentation.</p>
<p>A better version pushes the reaction out into a presenter or controller:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IPauseMenuView</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">event</span> Action ResumeClicked;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">event</span> Action OptionsClicked;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> SetPaused(<span style="color:#66d9ef">bool</span> paused);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PauseMenuPresenter</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IPauseMenuView _view;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IGameFlow _gameFlow;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> PauseMenuPresenter(IPauseMenuView view, IGameFlow gameFlow)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _view = view;
</span></span><span style="display:flex;"><span>        _gameFlow = gameFlow;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        _view.ResumeClicked += OnResumeClicked;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnResumeClicked()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _gameFlow.ResumeGame();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now the view is just a view. The presenter owns the reaction. The game flow object owns the rule.</p>
<p>That is the whole trick.</p>
<h2 id="let-one-thing-own-navigation">Let one thing own navigation</h2>
<p>A lot of UI spaghetti comes from letting every screen open and close every other screen directly.</p>
<p>That design gets messy fast.</p>
<p>One place should own screen flow. Sometimes that is a menu stack. Sometimes it is a small router. Sometimes it is a state machine. The exact shape matters less than the rule: <strong>the screen itself should not be responsible for the whole screen graph</strong>.</p>
<p>When a settings panel can open the pause menu, and the pause menu can open the store, and the store can close the HUD, the code stops being local. Every menu becomes a transit hub.</p>
<p>A better version looks like this:</p>
<ul>
<li>the button raises an event</li>
<li>the presenter decides what that event means</li>
<li>the flow controller opens or closes the next screen</li>
<li>the view reflects the current state</li>
</ul>
<p>That structure is boring, and that is praise. Boring UI code is easier to move, easier to test, and easier to delete when the feature changes.</p>
<h2 id="use-events-for-changes-not-polling-for-everything">Use events for changes, not polling for everything</h2>
<p>UI code that polls everything every frame is still common because it is easy to write once.</p>
<p>That is usually a mistake.</p>
<p>If a health label changes only when health changes, update it when health changes. If the currency display changes only after a reward, push the new value once instead of rebuilding text 60 times a second. If the inventory count changed, notify the view. If it did not change, leave it alone.</p>
<p>Event-driven UI has a few practical benefits:</p>
<ul>
<li>less per-frame work</li>
<li>fewer accidental dependencies</li>
<li>clearer data flow</li>
<li>easier profiling when the UI gets heavy</li>
</ul>
<p>This matters in games because UI often gets glued to hot paths without anyone noticing. A menu that rebuilds itself every tick is not a crisis in a tiny prototype. In a live game with layered HUDs, animation, localization, and platform prompts, it adds up fast.</p>
<p>If a constantly updating element is necessary, isolate it on purpose. Do not let it spread the habit to the rest of the UI.</p>
<h2 id="reuse-widgets-not-screen-controllers">Reuse widgets, not screen controllers</h2>
<p>Reusable widgets are useful, but the reuse has to stay narrow.</p>
<p>A button row, inventory slot, tab strip, tooltip, or player card should usually be a small component with a clear job. That part of the UI is worth reusing because the behavior is stable.</p>
<p>A whole screen controller is often too specific to reuse cleanly.</p>
<p>That distinction matters because the easiest way to make UI “modular” is to move the mess into a base class and hope nobody notices. That shortcut ages badly. It creates an inheritance tree that looks tidy until the first feature branch needs a slightly different screen flow and the whole thing starts bending.</p>
<p>Composition is usually the cleaner answer. Several small components that each own one piece of behavior are easier to understand than one inherited monster that tries to be every menu in the game.</p>
<p>For different menu layouts, assemble them from the same widgets and the same presenters. That keeps the surface area small.</p>
<h2 id="ownership-is-the-real-design-question">Ownership is the real design question</h2>
<p>Most UI mistakes are really ownership mistakes.</p>
<p>The useful questions are:</p>
<ul>
<li>who owns this state?</li>
<li>who is allowed to change it?</li>
<li>who should react when it changes?</li>
<li>who cleans it up?</li>
</ul>
<p>If those answers are hard to find, the UI architecture is already paying interest.</p>
<p>That is why hidden singletons, scene searches, and direct references into gameplay code cause so much pain. They blur the boundaries. A button should not need to know how to find the system that will do the real work. A screen should not have to discover the inventory manager by rummaging through the scene. A widget should not be the place where the game rules hide.</p>
<p>A better structure keeps those dependencies explicit and narrow.</p>
<h2 id="a-practical-screen-flow-example">A practical screen flow example</h2>
<p>A game with pause, settings, inventory, and store screens can stay manageable if the responsibilities stay separate.</p>
<p>One possible shape is:</p>
<ul>
<li>a <code>ScreenFlowController</code> owns which screen is on top</li>
<li>each screen exposes events like <code>ContinueClicked</code> or <code>BackClicked</code></li>
<li>each presenter listens to the view and tells the flow controller what should happen</li>
<li>the flow controller does not care how the screen was drawn</li>
<li>the screen does not care how the navigation stack works</li>
</ul>
<p>That separation makes the UI easier to refactor. If the pause menu turns into a modal stack tomorrow, only the flow layer changes. If the skin of the inventory screen changes, the presenter and flow logic can stay intact.</p>
<p>The result is not less code because the UI is simple. The result is better code because the UI can change without dragging the whole project with it.</p>
<h2 id="keep-game-logic-out-of-the-widget-tree">Keep game logic out of the widget tree</h2>
<p>The easiest way to create fragile UI is to let a widget perform game logic directly.</p>
<p>A button handler should not open save files, decide whether a mission is complete, validate prices, and update the HUD in one place. That is too much responsibility for a view.</p>
<p>The useful split is:</p>
<ul>
<li>the widget raises an event</li>
<li>the presenter turns the event into an action</li>
<li>the service or game system performs the action</li>
<li>the presenter or view updates based on the result</li>
</ul>
<p>That shape also makes testing easier. A presenter with a fake view and fake service can be checked without loading an entire scene. A direct button script buried in scene objects is much harder to exercise cleanly.</p>
<h2 id="rebuild-only-what-changed">Rebuild only what changed</h2>
<p>Another common UI problem is rebuilding more of the screen than necessary.</p>
<p>If a health value changes, the health label should update. The whole HUD does not need to be recreated. If the selected tab changes, the visible panel can swap. The entire menu graph does not need to be torn down and rebuilt.</p>
<p>This is where modularity helps beyond readability. Smaller, focused components make it easier to update only what changed. That keeps the UI cheaper to run and easier to reason about when a frame spike appears.</p>
<p>The same principle applies to complex views:</p>
<ul>
<li>update one widget when one value changes</li>
<li>refresh one panel when one state changes</li>
<li>rebuild one screen only when the screen itself changes</li>
</ul>
<p>That is the difference between responsive UI and expensive UI.</p>
<h2 id="localization-and-platform-prompts-belong-in-the-right-layer">Localization and platform prompts belong in the right layer</h2>
<p>UI often becomes fragile when it starts mixing content, presentation, and platform-specific concerns.</p>
<p>Localization strings should not be hardcoded inside button callbacks. Platform button prompts should not be scattered across gameplay scripts. Input hint mapping should not live in the same place as inventory logic.</p>
<p>Those concerns are part of presentation, but they are not the same as the widget tree. They belong in the layer that formats what the user sees, not in the layer that decides the game’s rules.</p>
<p>Keeping that distinction clear saves a lot of late-project pain when the game needs controller prompts, keyboard prompts, language switching, or accessibility changes.</p>
<h2 id="what-not-to-do">What not to do</h2>
<p>There are a few shortcuts that feel efficient and almost always become debt:</p>
<ul>
<li>reaching into gameplay singletons from button handlers</li>
<li>letting UI code search the scene for dependencies</li>
<li>putting save/load logic directly in widgets</li>
<li>mixing animation timing, navigation, and data formatting in one class</li>
<li>building a universal menu base before the screens actually share behavior</li>
<li>polling every value every frame when events would do the job</li>
</ul>
<p>The common problem is not style. It is ownership.</p>
<p>When one class knows too much, it becomes hard to answer simple questions like: who owns this state, who is allowed to replace it, and who cleans it up?</p>
<p>If those questions are hard to answer quickly, the architecture is already paying interest.</p>
<h2 id="a-practical-rule-of-thumb">A practical rule of thumb</h2>
<p>When it is unclear whether something belongs in the UI, ask one question:</p>
<p><strong>Does this code describe how the UI looks, or does it decide what the game means?</strong></p>
<p>If it is mostly look and interaction wiring, it belongs close to the view.
If it is about rules, flow, or state changes, it belongs outside the view.</p>
<p>That one line catches most of the mistakes in game UI code.</p>
<h2 id="the-tradeoff-is-worth-it">The tradeoff is worth it</h2>
<p>This structure is a little more work up front than wiring buttons directly to gameplay methods.</p>
<p>It is worth it once the project has more than a couple of screens.</p>
<p>The point is not elegance for its own sake. The point is survivability. UI changes constantly. Those changes should stay local. A menu system that can evolve without dragging half the game with it is worth a small amount of ceremony.</p>
<p>Not a cathedral. Just enough structure to keep the walls from collapsing.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>If the goal is modular, maintainable game UI, keep the responsibilities separate:</p>
<ul>
<li>a flow controller owns navigation</li>
<li>a presenter or controller owns reactions</li>
<li>a view owns rendering and input events</li>
<li>services and game state stay outside the UI layer</li>
</ul>
<p>That is usually enough to keep menus, HUDs, and in-game widgets from turning into a pile of hidden dependencies.</p>
<h2 id="references">References</h2>
<ul>
<li>Unity Manual, <a href="https://docs.unity3d.com/Manual/UI-system-compare.html">Comparison of UI systems in Unity</a></li>
<li>Unity App UI, <a href="https://docs.unity3d.com/Packages/com.unity.dt.app-ui@0.4/manual/mvvm-intro.html">Introduction to the MVVM Pattern</a></li>
<li>Yanko Oliveira, <a href="https://www.gamedeveloper.com/programming/a-ui-system-architecture-and-workflow-for-unity">A UI System Architecture and Workflow for Unity</a></li>
<li>Robert Nystrom, <a href="https://gameprogrammingpatterns.com/observer.html">Observer</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Code Smells and How to Fix &#39;Em</title>
            <link>https://blog.jonmikeys.com/posts/code-smells-and-how-to-fix-em/</link>
            <pubDate>Mon, 06 Jul 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/code-smells-and-how-to-fix-em/</guid>
            <description>A practical look at common code smells and the small changes that keep game code easier to read and maintain.</description>
            <content type="html"><![CDATA[<p>Code smells are the little warning signs that make code worth a second look. They are not always bugs, and they are not proof that a system is broken. More often, they mean the shape of the code is starting to cost more than it should. A function feels too wide. A parameter list turns into a receipt. The same logic shows up in three places. None of that is dramatic on its own, but each one makes the code harder to trust.</p>
<p>The useful part is that smells usually point somewhere specific. They do not ask for a heroic rewrite. They ask for a cleaner boundary, a smaller function, a shared helper, or a more honest data shape. In game projects, that matters a lot. Combat code, UI glue, state handling, and save logic all age quickly if the structure gets messy. Catching the smell early keeps the work small.</p>
<p><img src="/images/posts/code-smells-and-how-to-fix-em/diagram.svg" alt="Code Smells And How To Fix Em diagram"></p>
<h2 id="the-five-smells-worth-watching-first">The five smells worth watching first</h2>
<p>The ones that show up most often are simple: long functions, long parameter lists, duplicate code, god objects, and overengineering. Those are not the only smells worth learning, but they cover a lot of the everyday pain. They also tend to overlap. Long functions create duplication. Duplication often hides inside a giant manager. Long parameter lists usually mean the data shape is wrong. So if one of these shows up, it is worth checking for a second issue nearby.</p>
<p>That is why this topic is useful. It is not a list of forbidden patterns. It is a habit of noticing when code starts to feel sticky. Once that habit is in place, the fixes become fairly ordinary.</p>
<h2 id="long-functions-are-usually-doing-too-much">Long functions are usually doing too much</h2>
<p>A long function is not automatically bad. Sometimes a little extra length is exactly what the code needs. The problem starts when one function stops feeling like one idea and starts feeling like a pile of unrelated steps. That is when reading it becomes work.</p>
<p>This happens a lot in game code. A damage function starts out simple and ends up handling health, animation, sound, particles, quest updates, and save state. That is too many jobs in one place. The fix is usually not to make the code cleverer. It is to split the work into smaller pieces and give each piece a clear name.</p>
<p>Before:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> TakeDamage(<span style="color:#66d9ef">int</span> amount)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> damage = CalculateDamage(amount);
</span></span><span style="display:flex;"><span>    health -= damage;
</span></span><span style="display:flex;"><span>    UpdateHealthUI();
</span></span><span style="display:flex;"><span>    PlayHitAnimation();
</span></span><span style="display:flex;"><span>    PlayHitSound();
</span></span><span style="display:flex;"><span>    SpawnHitParticles();
</span></span><span style="display:flex;"><span>    NotifyQuestSystem();
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>After:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> TakeDamage(<span style="color:#66d9ef">int</span> amount)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> finalDamage = CalculateFinalDamage(amount);
</span></span><span style="display:flex;"><span>    ApplyHealthChange(finalDamage);
</span></span><span style="display:flex;"><span>    PlayDamageFeedback();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> PlayDamageFeedback()
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    PlayHitAnimation();
</span></span><span style="display:flex;"><span>    PlayHitSound();
</span></span><span style="display:flex;"><span>    SpawnHitParticles();
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The second version is easier to scan because the top-level intent is visible at once. The details are still there, but they are no longer fighting for attention.</p>
<h2 id="long-parameter-lists-usually-mean-the-data-shape-is-wrong">Long parameter lists usually mean the data shape is wrong</h2>
<p>A function with too many parameters is rarely just a cosmetic problem. It usually means the function is trying to know too much, or the data it needs really belongs together. Either way, the parameter list is the symptom.</p>
<p>Long argument lists make code harder to read and easier to misuse. The call site gets noisy. The order matters too much. Optional values become a guessing game. In game code, that slows everything down. A small utility should not feel like a form that needs to be filled out before it can run.</p>
<p>Before:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> GameObject FindNearestObject(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">float</span> radius,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">bool</span> includeEnemies,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">bool</span> includePlayers,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">bool</span> includeStaticObjects,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">bool</span> ignoreDisabled,
</span></span><span style="display:flex;"><span>    LayerMask layers)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// search logic here</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>After:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">SearchOptions</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> Radius;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">bool</span> IncludeEnemies;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">bool</span> IncludePlayers;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">bool</span> IncludeStaticObjects;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">bool</span> IgnoreDisabled;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> LayerMask Layers;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> GameObject FindNearestObject(SearchOptions options)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// search logic here</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is a small change, but it makes the intention easier to see. The data now has a shape of its own, and the function reads more like a request than a checklist.</p>
<h2 id="duplicate-code-is-expensive-in-the-boring-way">Duplicate code is expensive in the boring way</h2>
<p>Duplicate code does not always look dangerous at first. Copy a block, tweak it a little, move on. The cost shows up later, when the same rule changes in one place and not the others. Then the project starts behaving differently depending on which path the player took.</p>
<p>The fix is not to eliminate every repeated line. It is to look for repeated meaning. If two blocks are enforcing the same rule, there should probably be one place where that rule lives. That might be a helper function, a shared service, or a small common data path. The important part is consistency.</p>
<p>Before:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> ApplyFireDamage(<span style="color:#66d9ef">int</span> amount)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    health -= amount;
</span></span><span style="display:flex;"><span>    UpdateUI();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> ApplyIceDamage(<span style="color:#66d9ef">int</span> amount)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    health -= amount;
</span></span><span style="display:flex;"><span>    UpdateUI();
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>After:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> ApplyDamage(<span style="color:#66d9ef">int</span> amount)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    health -= amount;
</span></span><span style="display:flex;"><span>    UpdateUI();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> ApplyFireDamage(<span style="color:#66d9ef">int</span> amount) =&gt; ApplyDamage(amount);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> ApplyIceDamage(<span style="color:#66d9ef">int</span> amount) =&gt; ApplyDamage(amount);
</span></span></code></pre></div><p>That is not glamorous, but it keeps one rule in one place. In practice, that saves more time than most people expect.</p>
<h2 id="god-objects-make-everything-feel-connected-for-the-wrong-reason">God objects make everything feel connected for the wrong reason</h2>
<p>A god object is the class that knows too much and does too much. It owns the state, controls the flow, reaches into half the scene, and quietly becomes the place where every new feature lands. That feels convenient right up until it becomes impossible to change anything without worrying about five unrelated systems.</p>
<p>The real fix is boundary work. Separate orchestration from detail. Let one object coordinate the bigger flow, but do not make it personally responsible for every low-level task. Smaller collaborators are easier to understand and easier to replace.</p>
<p>A <code>GameManager</code> that owns input, player health, UI refreshes, quest progression, and save data is a good sign that the boundaries have gone blurry. That kind of object turns the project into one giant room where every conversation happens at once. The code may still work, but it stops being pleasant to work in.</p>
<p>Better boundaries usually mean less surprise later. When a system knows less, changes hurt less.</p>
<h2 id="overengineering-is-the-smell-that-looks-polished">Overengineering is the smell that looks polished</h2>
<p>Not every smell is a sign of too little structure. Sometimes the problem is too much structure. A system can become so abstract that nobody can tell what problem it is solving. That usually happens when flexibility gets added before the project has earned it.</p>
<p>This is the classic “maybe we will need it later” trap. Maybe we will. Maybe we will not. The safer move is to solve the problem in front of us, keep the seams visible, and leave room to extend the code later if that actually becomes necessary. A giant architecture project is a poor trade for a small feature.</p>
<p>That is the part worth remembering: overengineering is still a smell, even when the code looks tidy. Clean-looking code can still be harder to change than messy code if the abstraction is too broad.</p>
<h2 id="one-smell-often-leads-to-another">One smell often leads to another</h2>
<p>These problems tend to travel together. Long functions lead to duplication. Duplicate code hides inside giant managers. Long parameter lists usually mean the data shape is fighting the design. So when a section of code feels off, it is worth checking the neighboring pieces too.</p>
<p>That does not mean everything needs to be fixed in one pass. It usually goes better when the cleanup stays small. Trim the worst function first. Pull out the shared rule. Split the manager. Make the boundary clearer. A chain of small improvements is easier to finish than one grand rewrite.</p>
<h2 id="a-simple-rule-for-refactoring">A simple rule for refactoring</h2>
<p>When a code smell shows up, ask what the smell is actually telling you. Does the function need to shrink? Does the data need a better shape? Does the repeated logic want a shared home? Does the class need fewer responsibilities? Those questions usually point at the real fix faster than a vague feeling of “this code is messy.”</p>
<p>That is the useful part of code smells. They give a reason to pause before the code becomes harder to maintain than it needs to be. The goal is not perfect code. The goal is code that is easier to read, easier to change, and less annoying to come back to later.</p>
<h2 id="references">References</h2>
<ul>
<li>The Shaggy Dev — &ldquo;A few common code smells to be aware of and how to fix them&rdquo;
<a href="https://shaggydev.com/2023/01/16/code-smells/">https://shaggydev.com/2023/01/16/code-smells/</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Dependency Injection for Game Code: Keep Wiring at the Edges</title>
            <link>https://blog.jonmikeys.com/posts/dependency-injection-for-game-code/</link>
            <pubDate>Sun, 05 Jul 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/dependency-injection-for-game-code/</guid>
            <description>A practical look at dependency injection in game code, why it helps, and how to use it without turning your project into framework soup.</description>
            <content type="html"><![CDATA[<p>Dependency injection is one of those topics that sounds bigger than it is.</p>
<p>At the useful end of the scale, it is just this: build things at the edge of the game, then pass them into the code that needs them. Do not make your gameplay code go hunting through globals to find its own tools.</p>
<p>That sounds almost too simple, which is usually a good sign.</p>
<p><img src="/images/posts/dependency-injection-for-game-code/diagram.svg" alt="Dependency Injection For Game Code diagram"></p>
<h2 id="what-di-is-actually-doing">What DI is actually doing</h2>
<p>Dependency injection is about separating <strong>configuration</strong> from <strong>use</strong>.</p>
<p>A class should say what it needs. Something else should decide how to build those things and where they come from.</p>
<p>That is the core idea.</p>
<p>In a game, the thing doing the wiring is usually a bootstrapper, installer, scene initializer, or composition root. It creates the concrete services, then passes them into the systems that need them.</p>
<p>The gameplay code stays focused on gameplay.</p>
<h2 id="why-this-matters-in-games">Why this matters in games</h2>
<p>Game projects accumulate dependencies quickly.</p>
<p>A player controller may need:</p>
<ul>
<li>input</li>
<li>audio</li>
<li>animation</li>
<li>save data</li>
<li>inventory</li>
<li>targeting</li>
<li>camera control</li>
</ul>
<p>If every class reaches into a singleton or service locator, the code becomes hard to reason about.</p>
<p>You lose two things at once:</p>
<ol>
<li><strong>Clarity</strong> — you cannot see what a class needs just by looking at its constructor or setup.</li>
<li><strong>Testability</strong> — you cannot easily swap in a fake input system or fake audio sink.</li>
</ol>
<p>The result is code that is easy to call and hard to change.</p>
<h2 id="a-small-example">A small example</h2>
<p>Say you have a weapon system.</p>
<p>A bad version builds its own dependencies:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">WeaponFire</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Fire()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> audio = AudioSystem.Instance;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> rng = RandomService.Instance;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> projectiles = ProjectileDatabase.Instance;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        audio.Play(<span style="color:#e6db74">&#34;gunshot&#34;</span>);
</span></span><span style="display:flex;"><span>        projectiles.Spawn(rng.NextProjectile());
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That works, until it does not.</p>
<p>Now the class knows where audio comes from, where randomness comes from, and where projectiles come from. It is no longer just a weapon action. It is tied to your whole runtime.</p>
<p>A better version asks for what it needs:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IAudio</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Play(<span style="color:#66d9ef">string</span> cue);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IRng</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> Next(<span style="color:#66d9ef">int</span> max);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IProjectileFactory</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Spawn(<span style="color:#66d9ef">int</span> index);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">WeaponFire</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IAudio audio;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IRng rng;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IProjectileFactory projectiles;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> WeaponFire(IAudio audio, IRng rng, IProjectileFactory projectiles)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.audio = audio;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.rng = rng;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.projectiles = projectiles;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Fire()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        audio.Play(<span style="color:#e6db74">&#34;gunshot&#34;</span>);
</span></span><span style="display:flex;"><span>        projectiles.Spawn(rng.Next(<span style="color:#ae81ff">4</span>));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now the class reads like a contract.</p>
<p>If you want to test it, you can pass fake implementations. If you want to swap the audio backend, you change the wiring, not the weapon logic.</p>
<h2 id="the-composition-root-is-the-real-center">The composition root is the real center</h2>
<p>The cleanest DI setups have one place where the object graph gets assembled.</p>
<p>That place is the <strong>composition root</strong>.</p>
<p>In a game, that might be:</p>
<ul>
<li>the first scene loader</li>
<li>a bootstrapper object</li>
<li>a game mode initializer</li>
<li>a DI container installer</li>
</ul>
<p>That is where you create the concrete services:</p>
<ul>
<li><code>WwiseAudio</code> or <code>UnityAudio</code></li>
<li><code>InputSystem</code> or <code>GamepadInput</code></li>
<li><code>JsonSaveService</code></li>
<li><code>WeaponFactory</code></li>
<li><code>EnemySpawner</code></li>
</ul>
<p>Then you hand those services to the systems that use them.</p>
<p>The important part is that the wiring stays at the edge.</p>
<p>Gameplay code should not need to know whether the audio service comes from a singleton, a framework, or a plain <code>new</code> call in a bootstrapper.</p>
<h2 id="unity-changes-the-shape-not-the-rule">Unity changes the shape, not the rule</h2>
<p>Unity adds one annoying constraint: <code>MonoBehaviour</code> is not a normal class.</p>
<p>Unity’s own manual warns that if you try to define a constructor for a <code>MonoBehaviour</code>, it can interfere with normal operation. So if you are working in Unity, do not force constructor injection onto every component and pretend the engine does not exist.</p>
<p>Use DI where it fits naturally.</p>
<p>A good split is:</p>
<ul>
<li><strong>plain C# classes</strong> get constructor injection</li>
<li><strong>Unity components</strong> get wired through bootstrap code, serialized references, or explicit setup methods</li>
<li><strong>engine-facing code</strong> stays thin and hands work off to testable classes</li>
</ul>
<p>That keeps the engine boundary honest.</p>
<p>If your <code>MonoBehaviour</code> is doing all the real work, you are making your life harder than it needs to be.</p>
<h2 id="di-is-not-the-same-as-service-locator">DI is not the same as service locator</h2>
<p>People mix these up all the time.</p>
<p>A service locator says, &ldquo;Ask for whatever you want, whenever you want.&rdquo;</p>
<p>Dependency injection says, &ldquo;You get what you need up front.&rdquo;</p>
<p>That difference matters.</p>
<p>Service locator can be useful for truly ambient services, but it hides the dependency from the class that uses it. The code looks simpler while becoming more implicit.</p>
<p>DI keeps the dependency visible.</p>
<p>That visibility is the point.</p>
<p>If a class needs input, let it say so. If it needs audio, let it say so. If it needs save data, let it say so.</p>
<p>That is less magical, which is exactly why it works better.</p>
<h2 id="what-not-to-overbuild">What not to overbuild</h2>
<p>You do not need a framework to do dependency injection.</p>
<p>That is the mistake people make first.</p>
<p>They hear &ldquo;DI&rdquo; and reach for a container before they have even written a constructor.</p>
<p>Start smaller:</p>
<ul>
<li>pass dependencies into plain classes</li>
<li>use interfaces where there is a real seam</li>
<li>wire the graph once at startup</li>
<li>avoid globals unless the service is truly ambient</li>
</ul>
<p>If the project grows, a container may become worth it. If it does not, you have still kept the code sane.</p>
<p>Interfaces are not free either. Do not add one for every class out of habit. Add a seam when you actually need one: a second implementation, a test double, or a boundary you want to keep clean.</p>
<h2 id="practical-rule-of-thumb">Practical rule of thumb</h2>
<p>Use dependency injection when:</p>
<ul>
<li>a class needs stable collaborators</li>
<li>you want to test the class without the engine</li>
<li>you want to swap implementations by configuration</li>
<li>you want the object graph to be explicit</li>
</ul>
<p>Skip it when:</p>
<ul>
<li>the dependency is truly local and trivial</li>
<li>the class is a one-off glue object</li>
<li>the extra indirection buys you nothing</li>
</ul>
<p>That is the whole tradeoff.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Dependency injection is not about being fancy.</p>
<p>It is about keeping game code honest about what it needs.</p>
<p>When dependencies are wired at the edge, gameplay code becomes easier to read, easier to test, and easier to swap out later. That is especially valuable in games, where systems tend to grow sideways until the dependencies become the real problem.</p>
<p>Keep the wiring at the edge. Keep the behavior in the middle.</p>
<h2 id="references">References</h2>
<ul>
<li>Martin Fowler, <a href="https://martinfowler.com/articles/injection.html">Inversion of Control Containers and the Dependency Injection pattern</a></li>
<li>Unity Manual, <a href="https://docs.unity3d.com/6000.4/Documentation/Manual/class-MonoBehaviour.html">MonoBehaviour</a></li>
<li>Robert Nystrom, <a href="https://gameprogrammingpatterns.com/service-locator.html">Service Locator</a></li>
<li><a href="https://github.com/modesttree/Zenject">Zenject repository</a></li>
<li><a href="/posts/build-a-simple-di-container-in-csharp/">Build a Simple DI Container in C#</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Dependency Injection in Unity: Stop Letting Objects Hunt for Their Own Tools</title>
            <link>https://blog.jonmikeys.com/posts/dependency-injection-in-unity/</link>
            <pubDate>Sun, 05 Jul 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/dependency-injection-in-unity/</guid>
            <description>A practical way to make game code easier to test, refactor, and reason about.</description>
            <content type="html"><![CDATA[<p>A lot of Unity code starts the same way: a <code>MonoBehaviour</code> reaches into a singleton, grabs another component from the scene, pulls a manager out of static state, and then quietly grows into a knot nobody wants to touch.</p>
<p>It works. Right up until it doesn’t.</p>
<p>The problem is not that Unity is bad at dependency management. The problem is that most game code never decides who is responsible for wiring things together. Dependency injection is the boring answer to that problem, and boring is good here.</p>
<p><img src="/images/posts/dependency-injection-in-unity/diagram.svg" alt="Dependency Injection In Unity diagram"></p>
<h2 id="what-dependency-injection-actually-means">What dependency injection actually means</h2>
<p>Dependency injection is not “use a framework.”</p>
<p>It is simpler than that:</p>
<blockquote>
<p>A class should receive the things it needs from the outside instead of creating them or searching for them itself.</p>
</blockquote>
<p>That sounds small, but it changes the shape of the code.</p>
<p>Without DI, a class often knows:</p>
<ul>
<li>what it depends on</li>
<li>how to build those dependencies</li>
<li>where to find them</li>
<li>when they should be created</li>
<li>when they should be destroyed</li>
</ul>
<p>With DI, the class only knows:</p>
<ul>
<li>what it needs</li>
<li>how to use it</li>
</ul>
<p>That split matters because it separates <strong>configuration</strong> from <strong>behavior</strong>.</p>
<p>Martin Fowler’s classic DI article makes this point clearly: the important part is not the container, but the architectural rule that setup lives in one place and usage lives somewhere else.</p>
<h2 id="why-games-benefit-from-this-more-than-you-might-think">Why games benefit from this more than you might think</h2>
<p>Game code tends to accumulate “just one more reference” problems.</p>
<p>A gameplay system starts out small:</p>
<ul>
<li>health component</li>
<li>damage source</li>
<li>UI update</li>
<li>sound effect</li>
</ul>
<p>Then it grows:</p>
<ul>
<li>analytics</li>
<li>save data</li>
<li>difficulty tuning</li>
<li>tutorials</li>
<li>network replication</li>
<li>accessibility feedback</li>
</ul>
<p>If each system finds its own dependencies, the code becomes a hidden dependency graph. That graph still exists, but now it lives in scattered <code>FindObjectOfType</code>, singletons, static helpers, and scene-specific setup code.</p>
<p>That is where DI helps.</p>
<p>It makes the dependency graph visible.</p>
<p>And once the graph is visible, you can change it without playing archaeology.</p>
<h2 id="the-unity-specific-mistake-confusing-di-with-service-location">The Unity-specific mistake: confusing DI with service location</h2>
<p>This is the part people get wrong.</p>
<p>A common anti-pattern looks like DI but isn’t:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerHud</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Awake()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _gameState = ServiceLocator.Resolve&lt;GameState&gt;();
</span></span><span style="display:flex;"><span>        _audio = ServiceLocator.Resolve&lt;AudioService&gt;();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That code is not injection. It is just dependency hiding.</p>
<p>The class still depends on <code>GameState</code> and <code>AudioService</code>, but now the dependencies are implicit. You have made the code harder to read, harder to test, and harder to move.</p>
<p>Real DI makes dependencies obvious at the boundary.</p>
<p>Better:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerHudPresenter</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IGameState _gameState;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IAudioService _audio;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> PlayerHudPresenter(IGameState gameState, IAudioService audio)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _gameState = gameState;
</span></span><span style="display:flex;"><span>        _audio = audio;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now the constructor says what this object needs. No guessing. No global search. No hidden magic.</p>
<h2 id="what-to-inject-in-a-game">What to inject in a game</h2>
<p>Not everything needs DI. That would be silly.</p>
<p>Inject:</p>
<ul>
<li>services</li>
<li>repositories</li>
<li>command handlers</li>
<li>gameplay rules</li>
<li>data access</li>
<li>logging</li>
<li>save/load services</li>
<li>network facades</li>
<li>factories</li>
</ul>
<p>Do not inject everything just because you can.</p>
<p>Usually you should <strong>not</strong> inject:</p>
<ul>
<li>tiny local values</li>
<li>one-off temporary data</li>
<li>raw engine objects just because it feels “clean”</li>
<li>things the object can already own naturally</li>
</ul>
<p>A useful rule is this:</p>
<blockquote>
<p>Inject stable collaborators. Keep local state local.</p>
</blockquote>
<p>That keeps the constructor useful instead of turning it into a junk drawer.</p>
<h2 id="a-practical-unity-pattern-composition-root-plus-pure-c-systems">A practical Unity pattern: composition root plus pure C# systems</h2>
<p>Unity makes people nervous about DI because <code>MonoBehaviour</code> is not a normal class.</p>
<p>That is fair. But it does not block DI. It just means the wiring needs a clear home.</p>
<p>A good pattern is:</p>
<ol>
<li>Use a boot scene or composition root.</li>
<li>Build the object graph there.</li>
<li>Pass dependencies into pure C# systems.</li>
<li>Keep <code>MonoBehaviour</code> as the boundary layer.</li>
</ol>
<p>That gives you the best of both worlds:</p>
<ul>
<li>Unity handles lifecycle and scene objects</li>
<li>your gameplay logic stays testable and explicit</li>
</ul>
<h3 id="example">Example</h3>
<p>Imagine a simple inventory flow.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">IInventory</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">bool</span> HasItem(<span style="color:#66d9ef">string</span> id);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> Add(<span style="color:#66d9ef">string</span> id);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ISaveService</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">void</span> SaveInventory(IInventory inventory);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LootPickupSystem</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IInventory _inventory;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ISaveService _saveService;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> LootPickupSystem(IInventory inventory, ISaveService saveService)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _inventory = inventory;
</span></span><span style="display:flex;"><span>        _saveService = saveService;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Pickup(<span style="color:#66d9ef">string</span> itemId)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _inventory.Add(itemId);
</span></span><span style="display:flex;"><span>        _saveService.SaveInventory(_inventory);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This class is easy to test because it does not care about Unity. It cares about behavior.</p>
<p>Then a thin Unity entry point can wire it up:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">GameBootstrap</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> Inventory inventory;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> SaveService saveService;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> LootPickupSystem _lootPickupSystem;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> Awake()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _lootPickupSystem = <span style="color:#66d9ef">new</span> LootPickupSystem(inventory, saveService);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is manual injection. No framework required.</p>
<p>If the project grows, a container can reduce the glue code. But the design idea stays the same.</p>
<h2 id="manual-di-is-not-a-toy">Manual DI is not a toy</h2>
<p>A lot of developers jump straight to a container because they assume DI means a framework.</p>
<p>It doesn’t.</p>
<p>For smaller systems, manual DI is often the cleanest option:</p>
<ul>
<li>fewer moving parts</li>
<li>less magic</li>
<li>easier debugging</li>
<li>no container-specific learning curve</li>
</ul>
<p>If the object graph is tiny, just wire it by hand.</p>
<p>Use a container only when the composition starts getting repetitive or the object graph becomes annoying to maintain manually.</p>
<p>That is the lazy answer, and lazy is good when it means “don’t build machinery you do not need.”</p>
<h2 id="when-a-di-container-makes-sense">When a DI container makes sense</h2>
<p>A container starts earning its keep when you have:</p>
<ul>
<li>many services with layered dependencies</li>
<li>scoped lifetimes</li>
<li>runtime-created gameplay objects</li>
<li>separate presentation and domain layers</li>
<li>multiple scene roots</li>
<li>a need for easier testing and replacement</li>
</ul>
<p>In Unity, containers like VContainer are attractive because they support:</p>
<ul>
<li>constructor injection</li>
<li>scoped lifetime objects</li>
<li>pure C# entry points</li>
<li>runtime child scopes</li>
<li>cleaner setup for additive scenes or modular systems</li>
</ul>
<p>That said, a container is still a tool, not an architecture. If the design is bad, the container just helps wire the bad design faster.</p>
<h2 id="the-real-benefit-better-ownership">The real benefit: better ownership</h2>
<p>The hidden win of DI is not testability, although that is nice.</p>
<p>The real win is ownership clarity.</p>
<p>Ask these questions:</p>
<ul>
<li>Who creates this object?</li>
<li>Who configures it?</li>
<li>Who owns its lifetime?</li>
<li>Who can replace it?</li>
<li>Who is responsible for cleanup?</li>
</ul>
<p>DI forces those questions into the open.</p>
<p>That matters in games because game code often has messy lifetime boundaries:</p>
<ul>
<li>scene load</li>
<li>additive scene unload</li>
<li>menu vs gameplay</li>
<li>editor-only tools</li>
<li>runtime-generated entities</li>
<li>save/load transitions</li>
<li>network reconnects</li>
</ul>
<p>If you do not know where dependencies come from, cleanup becomes guesswork.</p>
<p>And guesswork becomes bugs.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<h3 id="1-injecting-everything">1. Injecting everything</h3>
<p>If every parameter is a dependency, the class is probably doing too much.</p>
<h3 id="2-using-di-to-hide-bad-architecture">2. Using DI to hide bad architecture</h3>
<p>A container will not save a system that mixes UI, gameplay rules, persistence, and network sync in one class.</p>
<h3 id="3-replacing-one-global-with-another">3. Replacing one global with another</h3>
<p>A service locator is just a global with a nicer outfit.</p>
<h3 id="4-making-constructors-unreadable">4. Making constructors unreadable</h3>
<p>If construction becomes a parade of unrelated services, split the class.</p>
<h3 id="5-treating-unity-objects-like-ordinary-objects">5. Treating Unity objects like ordinary objects</h3>
<p><code>MonoBehaviour</code> and <code>ScriptableObject</code> have engine lifecycles. Respect them instead of pretending they are plain C# instances.</p>
<h2 id="a-useful-mental-model-for-unity-projects">A useful mental model for Unity projects</h2>
<p>Think of your game in layers:</p>
<ul>
<li><strong>Presentation layer</strong>: MonoBehaviours, UI views, animation glue, scene objects</li>
<li><strong>Gameplay logic</strong>: rules, state transitions, commands, state machines</li>
<li><strong>Data/services</strong>: save/load, HTTP, analytics, audio, matchmaking, persistence</li>
</ul>
<p>Presentation should depend on gameplay logic.</p>
<p>Gameplay logic should depend on interfaces, not scene search.</p>
<p>Data/services should sit at the edges.</p>
<p>Dependency injection is the mechanism that keeps those boundaries honest.</p>
<h2 id="practical-example-ui-without-scene-spaghetti">Practical example: UI without scene spaghetti</h2>
<p>Suppose you are building a pause menu.</p>
<p>The wrong version:</p>
<ul>
<li>button code finds the audio manager</li>
<li>button code finds the game manager</li>
<li>button code changes time scale directly</li>
<li>button code decides how save/load works</li>
<li>button code knows too much</li>
</ul>
<p>The better version:</p>
<ul>
<li>the view raises a “ResumeClicked” event</li>
<li>a presenter or controller handles the action</li>
<li>the controller talks to injected services</li>
<li>the view stays dumb</li>
</ul>
<p>That structure is easier to maintain because UI stops being the place where all game rules go to die.</p>
<p>This is one reason DI and modular UI systems go together so well.</p>
<h2 id="when-not-to-use-di">When not to use DI</h2>
<p>Do not force DI into:</p>
<ul>
<li>trivial throwaway tools</li>
<li>tiny prototypes that will be deleted tomorrow</li>
<li>local state that is naturally owned by one object</li>
<li>code where the dependency is obvious and immutable already</li>
</ul>
<p>If DI makes the code heavier than the problem, skip it.</p>
<p>The goal is not purity. The goal is clarity.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li>Dependency injection is about <strong>receiving dependencies from outside</strong>, not building them inside.</li>
<li>The big win is <strong>separating configuration from use</strong>.</li>
<li>In Unity, a <strong>composition root</strong> or boot scene keeps the wiring centralized.</li>
<li>Manual injection is often enough for small systems.</li>
<li>A container helps when the object graph and lifetimes get more complex.</li>
<li>Do not confuse DI with service location. Hidden dependencies are still hidden, even if they are behind a fancy API.</li>
</ul>
<p>If you want cleaner game code, start by asking one question: <strong>should this class know how to find what it needs?</strong><br>
Most of the time, the answer is no.</p>
<p>Related reading: <a href="/posts/build-a-simple-di-container-in-csharp/">Build a Simple DI Container in C#</a> walks through what actually happens inside the container. <a href="/posts/reflex-di-factories-abstract-factories/">Combining Reflex DI with Factories</a> shows the same idea applied to runtime factories in Unity.</p>
]]></content>
        </item>
        
        <item>
            <title>Using the Observer Pattern in Games</title>
            <link>https://blog.jonmikeys.com/posts/observer-pattern-in-games/</link>
            <pubDate>Sun, 05 Jul 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/observer-pattern-in-games/</guid>
            <description>A practical look at the observer pattern, event buses, and why game objects should communicate without dragging the whole scene graph into the conversation.</description>
            <content type="html"><![CDATA[<p>One of the most common problems you run into in game development is figuring out how two unrelated objects should communicate without making the code brittle.</p>
<p>Maybe an enemy dies and the score should update. Maybe the player enters an arena and the gate should lock. Maybe the UI should light up when a boss starts. If you wire those systems together with direct calls everywhere, the code gets tangled very quickly.</p>
<p>That is exactly the kind of situation where the observer pattern helps.</p>
<p><img src="/images/posts/observer-pattern-in-games/diagram.svg" alt="Observer Pattern In Games diagram"></p>
<h2 id="the-core-idea">The core idea</h2>
<p>Observer works well as a subscription list.</p>
<p>One object produces an event.
One or more other objects care about that event.
The producer should not need to know who is listening.</p>
<p>That sounds small, but it changes the shape of the code.</p>
<p>Instead of asking, “How do I reach into the UI from gameplay code?”, you ask, “What happened?” and let the interested systems react.</p>
<p>That gives you a much cleaner boundary.</p>
<h2 id="what-the-pattern-looks-like">What the pattern looks like</h2>
<p>At the core, observer is just a subject that keeps track of listeners and notifies them when something interesting happens.</p>
<p>In C#, that often looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlayerEnteredArenaSignal</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">event</span> Action? Raised;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> Fire()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        Raised?.Invoke();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>And then another system subscribes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ArenaGate</span> : MonoBehaviour
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [SerializeField]</span> <span style="color:#66d9ef">private</span> PlayerEnteredArenaSignal signal = <span style="color:#66d9ef">null</span>!;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnEnable()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        signal.Raised += LockGate;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> OnDisable()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        signal.Raised -= LockGate;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">void</span> LockGate()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Close the gate, play animation, maybe trigger a sound.</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The gate does not need to know who detected the player.
The detector does not need to know how the gate works.
That is the whole point.</p>
<h2 id="why-this-beats-direct-references-everywhere">Why this beats direct references everywhere</h2>
<p>Direct references are fine when objects are tightly related and live near each other.
If a door and a trigger volume are part of the same arena setup, passing a reference can be the simplest thing that works.</p>
<p>The trouble starts when that same style spreads through the whole project.</p>
<p>Then you end up with:</p>
<ul>
<li>gameplay code calling UI code directly</li>
<li>UI code reaching back into gameplay state</li>
<li>audio code depending on scene-specific objects</li>
<li>one object knowing too much about the rest of the level</li>
</ul>
<p>That is how “just one more reference” turns into scene spaghetti.</p>
<p>Observer helps because it turns communication into a contract:</p>
<blockquote>
<p>When this happens, whoever cares may react.</p>
</blockquote>
<p>That keeps the producer focused on its own job.
It also makes the consumers easier to replace.</p>
<h2 id="nearby-communication-vs-distant-communication">Nearby communication vs distant communication</h2>
<p>Not every connection should be solved the same way.</p>
<p>If two objects are close in the hierarchy, a direct reference is often enough.
If they are far apart, something looser usually fits better.</p>
<p>A rough layout might look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Game
</span></span><span style="display:flex;"><span>├─ Level
</span></span><span style="display:flex;"><span>│  ├─ Arena
</span></span><span style="display:flex;"><span>│  │  ├─ EntranceDetector
</span></span><span style="display:flex;"><span>│  │  └─ Gate
</span></span><span style="display:flex;"><span>└─ UI
</span></span><span style="display:flex;"><span>   └─ BossHUD
</span></span></code></pre></div><p>The gate and the detector can probably talk through a local reference.
The boss HUD should not know anything about the arena layout.</p>
<p>That is where a broader event system or event bus makes sense.</p>
<h2 id="observer-vs-event-bus">Observer vs event bus</h2>
<p>People often blur these together, but they are not the same thing.</p>
<ul>
<li><strong>Observer</strong>: one subject, many listeners</li>
<li><strong>Event bus</strong>: a shared place where unrelated systems publish and subscribe to named events</li>
</ul>
<p>Observer is great when the relationship is local and obvious.
An event bus is useful when the sender and receiver live far apart.</p>
<p>Splitting buses by concern is better than making one giant global dumping ground.
A single <code>GlobalEventBus</code> becomes a junk drawer quickly.
An <code>AudioEvents</code>, <code>UIEvents</code>, or <code>GameplayEvents</code> bus is at least honest about scope.</p>
<h2 id="the-unity-version-of-this-pattern">The Unity version of this pattern</h2>
<p>In Unity, this pattern usually shows up in one of three forms:</p>
<ol>
<li><strong>C# events / delegates</strong></li>
<li><strong>UnityEvents</strong></li>
<li><strong>ScriptableObject event channels or custom event services</strong></li>
</ol>
<p>For most gameplay code, plain C# events are still the least surprising option.
They are simple, fast enough for normal gameplay code, and easy to read.</p>
<p>The important part is not the exact API.
It is the rule behind it:</p>
<ul>
<li>subscribe when the object becomes active</li>
<li>unsubscribe when it goes away</li>
<li>do not keep phantom listeners around</li>
</ul>
<p>That last part matters more than people think.
A forgotten subscription is how you get ghost callbacks and objects reacting long after they should be dead.</p>
<h2 id="a-practical-example-arena-gate-and-boss-ui">A practical example: arena gate and boss UI</h2>
<p>Suppose your level has three pieces:</p>
<ul>
<li>a trigger that notices the player entered the arena</li>
<li>a gate that should lock</li>
<li>a boss UI that should appear</li>
</ul>
<p>The trigger raises one event.
The gate listens.
The UI listens.</p>
<p>None of them needs to call the others directly.</p>
<p>That buys you a few things:</p>
<ul>
<li>the trigger can be reused in another level</li>
<li>the gate can be reused for another encounter</li>
<li>the UI can be swapped without rewriting the arena logic</li>
</ul>
<p>That is the sort of reuse that matters in game production.
Not theoretical reuse.
Practical reuse.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<h3 id="1-using-observer-everywhere">1. Using observer everywhere</h3>
<p>If everything is an event, nothing is clear.</p>
<p>A direct reference is still the best solution for many local relationships.
Use observer when the coupling actually hurts you.</p>
<h3 id="2-forgetting-to-unsubscribe">2. Forgetting to unsubscribe</h3>
<p>This is the classic Unity mistake.
If an object subscribes in <code>OnEnable</code>, it should usually unsubscribe in <code>OnDisable</code>.</p>
<h3 id="3-turning-events-into-hidden-state-machines">3. Turning events into hidden state machines</h3>
<p>If a system only makes sense after you read six event handlers and a bus implementation, the design is probably too clever.</p>
<h3 id="4-making-the-event-payload-too-vague">4. Making the event payload too vague</h3>
<p>“Something happened” is not enough for long.
Give the listener enough context to do its job.
If a boss died, the listener may need the boss ID, the killer, or the arena stage.</p>
<h3 id="5-building-one-universal-bus">5. Building one universal bus</h3>
<p>A single global bus feels convenient until it becomes impossible to reason about.
Smaller, domain-specific buses are usually easier to maintain.</p>
<h2 id="what-to-remember">What to remember</h2>
<p>The observer pattern is not magic.
It is just a clean way to say:</p>
<ul>
<li>one thing changed</li>
<li>some other things may care</li>
<li>let them react without hard-wiring them together</li>
</ul>
<p>That makes it a strong fit for UI updates, achievement tracking, enemy deaths, quest progression, arena logic, and a lot of other game systems that want to stay loosely coupled.</p>
<p>The real lesson is not just “use events.”
It is “be intentional about communication paths.”</p>
<p>Direct references are fine when the relationship is local.
Observer is better when the relationship should stay loose.
Event buses are useful when the objects are too far apart to know about each other directly.</p>
<p>Use the smallest tool that keeps the code honest.</p>
<h2 id="references--further-reading">References / Further reading</h2>
<ul>
<li>Microsoft Learn: <strong>Observer design pattern</strong>
<a href="https://learn.microsoft.com/en-us/dotnet/standard/events/observer-design-pattern">https://learn.microsoft.com/en-us/dotnet/standard/events/observer-design-pattern</a></li>
<li>Game Programming Patterns: <strong>Observer</strong>
<a href="https://gameprogrammingpatterns.com/observer.html">https://gameprogrammingpatterns.com/observer.html</a></li>
<li>Unity documentation: <strong>UnityEvents</strong>
<a href="https://docs.unity3d.com/Manual/UnityEvents.html">https://docs.unity3d.com/Manual/UnityEvents.html</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>A* Pathfinding: A Practical Guide for Game Developers</title>
            <link>https://blog.jonmikeys.com/posts/astar-pathfinding/</link>
            <pubDate>Sun, 21 Jun 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/astar-pathfinding/</guid>
            <description>A practical explanation of A* pathfinding, why it works, and how to use it in game projects.</description>
            <content type="html"><![CDATA[<p>A* is one of those algorithms people hear about early and then keep using for years.</p>
<p>That is usually because it solves a real problem: given a start and a target, find a good path without exploring the whole map. It is not the only way to do pathfinding, but it is a strong default for game AI because it balances speed and path quality well.</p>
<p>If you are building games in Unity or another engine, A* is worth understanding even if you later use a library. It helps you debug bad paths, tune movement systems, and choose the right grid or navmesh setup.</p>
<p><img src="/images/posts/astar-pathfinding/diagram.svg" alt="Astar Pathfinding diagram"></p>
<h2 id="what-a-is-trying-to-do">What A* is trying to do</h2>
<p>A pathfinder has two jobs:</p>
<ol>
<li>Avoid missing the best route.</li>
<li>Avoid wasting time checking obviously bad routes.</li>
</ol>
<p>A* does both by scoring each candidate node with two values:</p>
<ul>
<li><strong>g</strong> = cost from the start to this node</li>
<li><strong>h</strong> = estimated cost from this node to the goal</li>
</ul>
<p>Then it combines them:</p>
<ul>
<li><strong>f = g + h</strong></li>
</ul>
<p>The node with the lowest <code>f</code> is the most attractive next step.</p>
<p>That is the core idea. A* is not “smart” because it knows the answer ahead of time. It is smart because it keeps a running cost from the start and a guess toward the goal, then prefers nodes that look promising on both sides.</p>
<h2 id="why-the-heuristic-matters">Why the heuristic matters</h2>
<p>The <code>h</code> value is called the heuristic.</p>
<p>For a grid, common heuristics are:</p>
<ul>
<li><strong>Manhattan distance</strong> for four-direction movement</li>
<li><strong>Diagonal / octile distance</strong> for eight-direction movement</li>
<li><strong>Euclidean distance</strong> for free movement</li>
</ul>
<p>The heuristic should be cheap to compute and should not overestimate the real remaining cost if you want guaranteed optimal paths. That property is called <strong>admissible</strong>.</p>
<p>If the heuristic is too low, A* still works, but it may search more nodes than necessary. If it is too high, the algorithm may become faster but can start returning suboptimal paths.</p>
<p>In games, “perfectly optimal” is not always the goal. Sometimes “good enough and fast” is the better tradeoff. Just be deliberate about it.</p>
<h2 id="how-the-search-actually-runs">How the search actually runs</h2>
<p>A* usually uses two sets:</p>
<ul>
<li><strong>Open set</strong>: nodes discovered but not fully processed</li>
<li><strong>Closed set</strong>: nodes already processed</li>
</ul>
<p>The flow is simple:</p>
<ol>
<li>Put the start node in the open set.</li>
<li>Pick the open node with the lowest <code>f</code>.</li>
<li>If that node is the goal, stop and rebuild the path.</li>
<li>Otherwise, move it to the closed set.</li>
<li>Check each neighbor.</li>
<li>If a better route to a neighbor is found, update its <code>g</code>, <code>h</code>, and parent node.</li>
<li>Repeat.</li>
</ol>
<p>The parent pointer is what lets you reconstruct the final path after the goal is found. You walk backward from the goal node to the start, then reverse the list.</p>
<h2 id="a-small-grid-example">A small grid example</h2>
<p>Imagine a top-down game with a grid map. Each tile is a node.</p>
<p>If a wall blocks the direct route, A* will still try nearby tiles that seem promising. It does not blindly spiral outward the way an uninformed search might. It keeps pushing toward the target while still respecting the true movement cost it has already paid.</p>
<p>That is why A* feels efficient in practice. It spends its effort where it matters.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<p>A few mistakes show up often in game projects:</p>
<h3 id="1-using-the-wrong-heuristic">1. Using the wrong heuristic</h3>
<p>If your movement allows diagonals, Manhattan distance can be a poor fit. Match the heuristic to the movement model.</p>
<h3 id="2-ignoring-movement-costs">2. Ignoring movement costs</h3>
<p>Not all terrain should cost the same. Mud, stairs, water, and slopes may need higher traversal costs. A* can handle that, but only if your node costs reflect the world.</p>
<h3 id="3-rebuilding-paths-too-often">3. Rebuilding paths too often</h3>
<p>If an NPC recalculates every frame for no reason, pathfinding becomes a hidden performance problem. Repath when the target changes, the path becomes invalid, or the agent gets stuck.</p>
<h3 id="4-treating-nav-data-as-static-when-it-is-not">4. Treating nav data as static when it is not</h3>
<p>Doors open. Bridges collapse. Other agents block corridors. If your world changes, your pathing data needs to change too.</p>
<h3 id="5-forgetting-path-smoothing">5. Forgetting path smoothing</h3>
<p>A* often returns a path that is correct but ugly. In a real game, you may want to simplify waypoints, smooth corners, or blend movement so characters do not look like they are walking a chessboard.</p>
<h2 id="when-a-is-the-right-tool">When A* is the right tool</h2>
<p>Use A* when:</p>
<ul>
<li>You have a known map or navigation graph</li>
<li>You want reasonably optimal paths</li>
<li>The world is large enough that brute force is too expensive</li>
<li>You need predictable behavior</li>
</ul>
<p>It is especially useful for tile maps, tactical games, top-down games, and any system where nodes and costs are easy to define.</p>
<p>If your world is mostly open and continuous, a navmesh or steering system may fit better. If your world is tiny and dynamic, a simpler approach may be enough.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>A* works because it keeps two truths in mind at once:</p>
<ul>
<li>how much you have already paid</li>
<li>how likely a route is to lead to the goal</li>
</ul>
<p>That is a simple idea, but it scales well.</p>
<p>If you are building game AI, learn A* once, then reuse that knowledge everywhere. Even when you move to navmeshes, hierarchical pathfinding, or custom routing, the same cost-thinking still applies.</p>
<p>For volumetric worlds, <a href="/posts/octrees-for-faster-3d-pathfinding/">Octrees for Faster 3D Pathfinding</a> shows how an adaptive spatial graph can reduce the number of nodes A* needs to search.</p>
<h2 id="diagram-note">Diagram note</h2>
<p>The grid diagram above is the main visual for this post. If you add a second diagram later, a tiny node-cost breakdown can show <code>g</code>, <code>h</code>, and <code>f</code> for one tile.</p>
]]></content>
        </item>
        
        <item>
            <title>Behavior Trees: A Cleaner Way to Organize Game AI</title>
            <link>https://blog.jonmikeys.com/posts/behavior-trees-for-game-ai/</link>
            <pubDate>Sun, 21 Jun 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/behavior-trees-for-game-ai/</guid>
            <description>A practical introduction to behavior trees, how they work, and when they are a better fit than state machines.</description>
            <content type="html"><![CDATA[<p>Behavior trees are a structured way to decide what an AI should do next.</p>
<p>They are popular in games because they are easier to read than a giant state machine and easier to debug than a pile of ad hoc <code>if</code> statements. They do not replace every other AI system, but they are a very good tool when you want behavior that is modular, hierarchical, and easy to inspect.</p>
<p>If you have worked with Unity, you may have seen behavior trees used for enemies, companions, or utility layers around animation and combat. The main reason they stick around is simple: they scale better than many hand-built decision chains.</p>
<p><img src="/images/posts/behavior-trees-for-game-ai/diagram.svg" alt="Behavior Trees For Game Ai diagram"></p>
<h2 id="the-basic-idea">The basic idea</h2>
<p>A behavior tree is made of nodes.</p>
<p>There are a few common node types:</p>
<ul>
<li><strong>Sequence</strong>: run children in order; fail if one child fails</li>
<li><strong>Selector</strong>: run children in order; succeed if one child succeeds</li>
<li><strong>Decorator</strong>: modify a child’s behavior</li>
<li><strong>Leaf / action</strong>: do the actual work</li>
</ul>
<p>The tree is usually ticked every frame or on a fixed update. Each tick asks, “What should I do right now?”</p>
<p>That means the AI is not trying to solve everything in one giant decision. It walks the tree and finds the next useful action.</p>
<h2 id="why-behavior-trees-work-well">Why behavior trees work well</h2>
<p>Behavior trees have two strengths that matter a lot in game code:</p>
<h3 id="1-they-are-readable">1. They are readable</h3>
<p>A designer or programmer can look at the tree and see the structure of the behavior.</p>
<p>For example:</p>
<ul>
<li>If the player is visible
<ul>
<li>Move to target</li>
<li>Attack</li>
</ul>
</li>
<li>Otherwise
<ul>
<li>Patrol</li>
</ul>
</li>
</ul>
<p>That is much easier to reason about than a spread of nested conditions across several scripts.</p>
<h3 id="2-they-are-composable">2. They are composable</h3>
<p>Small behaviors can be reused.</p>
<p>A “move to cover” action can appear in multiple branches. A “has line of sight?” check can sit above several combat actions. You do not need to duplicate the logic every time.</p>
<p>That is the big advantage over simple monolithic AI code.</p>
<h2 id="a-simple-example">A simple example</h2>
<p>Imagine a guard enemy.</p>
<p>A rough tree might look like this:</p>
<ul>
<li>Selector
<ul>
<li>Sequence
<ul>
<li>If player visible</li>
<li>If in attack range</li>
<li>Attack</li>
</ul>
</li>
<li>Sequence
<ul>
<li>If player visible</li>
<li>Move toward player</li>
</ul>
</li>
<li>Patrol</li>
</ul>
</li>
</ul>
<p>This reads almost like English.</p>
<p>If the guard sees the player and is close enough, it attacks. If the guard sees the player but is too far away, it moves closer. If it does not see the player, it patrols.</p>
<p>That is the kind of behavior tree logic people usually want: clear, layered, and easy to extend.</p>
<h2 id="behavior-trees-vs-finite-state-machines">Behavior trees vs finite state machines</h2>
<p>This comparison comes up a lot.</p>
<p>A finite state machine says, “The AI is in one state at a time.”</p>
<p>A behavior tree says, “The AI evaluates a hierarchy of decisions and finds the best available branch.”</p>
<p>FSMs are good when the behavior is small and stable. Behavior trees are better when the number of behaviors and transitions starts growing.</p>
<p>The difference is not just style. It affects maintenance.</p>
<p>In an FSM, adding one new feature can mean adding transitions between several states.</p>
<p>In a behavior tree, you often add one new branch or leaf and place it in the hierarchy where it belongs.</p>
<p>That is usually easier to scale.</p>
<h2 id="practical-implementation-tips">Practical implementation tips</h2>
<p>If you are building behavior trees in Unity, keep these rules in mind:</p>
<h3 id="keep-leaves-small">Keep leaves small</h3>
<p>Leaf nodes should do one job well.</p>
<p>Bad leaf:</p>
<ul>
<li>“Chase target and aim and decide when to shoot and handle recoil”</li>
</ul>
<p>Better leaves:</p>
<ul>
<li>“Move to target”</li>
<li>“Aim at target”</li>
<li>“Fire weapon”</li>
</ul>
<p>Small leaves are easier to test and reuse.</p>
<h3 id="use-a-blackboard">Use a blackboard</h3>
<p>A blackboard is shared data the tree can read and write.</p>
<p>Examples:</p>
<ul>
<li>current target</li>
<li>last seen position</li>
<li>patrol route</li>
<li>health</li>
<li>ammo count</li>
</ul>
<p>Without a shared data layer, your tree nodes start depending on each other in messy ways. A fuller note on that pattern lives here: <a href="/posts/blackboard-architecture-for-game-ai/">Blackboard Architecture for Game AI</a>.</p>
<h3 id="do-not-make-the-tree-too-clever">Do not make the tree too clever</h3>
<p>A behavior tree should be understandable at a glance.</p>
<p>If the logic becomes full of special cases, the tree loses one of its biggest advantages. Keep the hierarchy simple and let the world state do some of the work.</p>
<h3 id="think-about-interruption">Think about interruption</h3>
<p>Good AI often needs to switch priorities.</p>
<p>If the player appears while the NPC is patrolling, the tree should be able to move from “patrol” to “combat” without weird delays.</p>
<p>This is where priority selectors and decorators become useful.</p>
<h2 id="where-behavior-trees-fit-best">Where behavior trees fit best</h2>
<p>Behavior trees are a strong choice when:</p>
<ul>
<li>you want clear, maintainable AI logic</li>
<li>the behavior is layered and hierarchical</li>
<li>designers or other programmers may need to read and edit it</li>
<li>you want the tree to be visible in tooling or debugging views</li>
</ul>
<p>They are often a good fit for enemies, followers, bosses, and NPCs with a few distinct modes of behavior.</p>
<p>They are less useful when the agent needs to plan long chains of actions from world goals. In that case, GOAP may fit better.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>Behavior trees are popular because they make AI logic easier to organize.</p>
<p>They do not make the AI “smarter” by themselves. They just give you a cleaner structure for deciding what happens next.</p>
<p>That is often enough to make a game AI feel more reliable and much easier to work on.</p>
<h2 id="diagram-note">Diagram note</h2>
<p>The tree diagram above is the right visual for this post. If you add a second image later, a tick-trace or blackboard-state snapshot would be the next most useful one.</p>
]]></content>
        </item>
        
        <item>
            <title>Blackboard Architecture for Game AI: Shared State Without the Mess</title>
            <link>https://blog.jonmikeys.com/posts/blackboard-architecture-for-game-ai/</link>
            <pubDate>Sun, 21 Jun 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/blackboard-architecture-for-game-ai/</guid>
            <description>A practical explanation of blackboard architecture for game AI, why it exists, and how it helps behavior trees and GOAP share state cleanly.</description>
            <content type="html"><![CDATA[<p>A blackboard is a shared place for AI facts.</p>
<p>It is not the AI itself. It is the data the AI agrees to read and write.</p>
<p><img src="/images/posts/blackboard-architecture-for-game-ai/diagram.svg" alt="Blackboard Architecture For Game Ai diagram"></p>
<h2 id="what-a-blackboard-does">What a blackboard does</h2>
<p>A blackboard sits between the systems that notice things and the systems that decide what to do.</p>
<p>A sensor can write, &ldquo;the player was seen here.&rdquo; A combat system can write, &ldquo;ammo is low.&rdquo; A movement system can write, &ldquo;cover is available.&rdquo; Then a behavior tree, GOAP planner, or utility system can read those facts without needing direct links to every writer.</p>
<p>That separation is the whole point.</p>
<h2 id="why-it-exists">Why it exists</h2>
<p>Game AI gets messy when every decision node reaches into every other system.</p>
<p>Without a blackboard, code tends to grow hard-coded dependencies:</p>
<ul>
<li>the patrol node asks the vision system directly</li>
<li>the attack node asks the weapon system directly</li>
<li>the flee node asks the health system directly</li>
<li>the planner has to know where every fact lives</li>
</ul>
<p>That works for a small prototype. It becomes a problem when the AI needs to grow.</p>
<p>A blackboard gives you one shared place for transient facts. The decision system can stay focused on choosing actions instead of hunting through unrelated components.</p>
<h2 id="what-belongs-on-a-blackboard">What belongs on a blackboard</h2>
<p>Good blackboard values are short-lived and useful to more than one system.</p>
<p>Examples:</p>
<ul>
<li>current target</li>
<li>last seen player position</li>
<li>current goal</li>
<li>cover location</li>
<li>ammo count</li>
<li>health threshold</li>
<li>alert level</li>
<li>timer since contact</li>
</ul>
<p>Those values matter because they are part of the current decision, not permanent game state.</p>
<h2 id="what-should-not-live-there">What should not live there</h2>
<p>A blackboard is not a junk drawer.</p>
<p>Do not use it for everything.</p>
<p>Bad candidates include:</p>
<ul>
<li>stable save data</li>
<li>large inventory graphs</li>
<li>long-term quest history</li>
<li>full object ownership rules</li>
<li>state that belongs inside a dedicated component</li>
</ul>
<p>If the value is persistent or strongly owned by one system, keep it there. The blackboard should hold the facts that multiple AI systems need to share right now.</p>
<h2 id="why-behavior-trees-like-blackboards">Why behavior trees like blackboards</h2>
<p>Behavior trees work well with blackboards because the tree can stay readable while the data stays centralized.</p>
<p>A branch can check:</p>
<ul>
<li>is the player visible?</li>
<li>is health low?</li>
<li>is cover available?</li>
<li>is a target already set?</li>
</ul>
<p>The tree does not need to know where those facts came from. It only needs to know whether they are true.</p>
<p>That keeps the tree small and the data flow easier to follow.</p>
<h2 id="why-goap-likes-blackboards">Why GOAP likes blackboards</h2>
<p>GOAP also benefits from a blackboard, but in a different way.</p>
<p>The planner needs a world state. The action runner needs current facts. Replanning needs a quick way to tell whether the world has changed enough to matter.</p>
<p>That is why a lot of GOAP setups end up with a blackboard or shared state object.</p>
<p>The blackboard is where the agent keeps the facts that the planner can use to build a plan.</p>
<p>If you want the GOAP side in more detail, see <a href="/posts/goap-for-game-ai/">GOAP for Game AI: Let Actions Build the Plan</a>. The shared-state version shows up in the implementation section there.</p>
<h2 id="a-simple-unity-shape">A simple Unity shape</h2>
<p>In Unity, a blackboard is often just a component or plain data object attached to the agent.</p>
<p>A practical setup looks like this:</p>
<ul>
<li>sensors update blackboard values</li>
<li>decision logic reads those values</li>
<li>actions write back when they change state</li>
<li>debug views inspect the same object</li>
</ul>
<p>That setup is simple, but it gives you a clean place to inspect what the AI thinks is true.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<p>The most common mistake is treating the blackboard like a global singleton.</p>
<p>That makes debugging easier at first and harder later.</p>
<p>Other mistakes include:</p>
<ul>
<li>storing too much unrelated data</li>
<li>letting every system write every field</li>
<li>using unclear names</li>
<li>mixing transient facts with long-term game state</li>
<li>forgetting who owns a value</li>
</ul>
<p>A blackboard works best when it stays narrow and intentional.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>A blackboard does not make AI smarter by itself.</p>
<p>It makes the system easier to organize.</p>
<p>That is the real value: behavior trees, GOAP, and other AI systems can share facts without becoming tightly coupled to the rest of the game.</p>
<p>If the AI needs to react to changing conditions, a blackboard is usually one of the first pieces worth adding.</p>
]]></content>
        </item>
        
        <item>
            <title>GOAP for Game AI: Let Actions Build the Plan</title>
            <link>https://blog.jonmikeys.com/posts/goap-for-game-ai/</link>
            <pubDate>Sun, 21 Jun 2026 00:00:00 +0000</pubDate>
            
            <guid>https://blog.jonmikeys.com/posts/goap-for-game-ai/</guid>
            <description>An introduction to GOAP, how it differs from behavior trees and FSMs, and how to use it in game AI.</description>
            <content type="html"><![CDATA[<p>GOAP stands for <strong>Goal-Oriented Action Planning</strong>.</p>
<p>It is a way to build game AI that chooses what to do by planning from the current world state toward a goal. Instead of hardcoding one fixed behavior chain, you describe the actions an agent can take, the conditions required for each action, and the effects those actions produce.</p>
<p>Then the planner figures out the sequence.</p>
<p>That is the whole appeal. You define the pieces. The AI assembles the plan.</p>
<p><img src="/images/posts/goap-for-game-ai/diagram.svg" alt="Goap For Game Ai diagram"></p>
<h2 id="why-goap-exists">Why GOAP exists</h2>
<p>A lot of game AI starts as a finite state machine.</p>
<p>That works fine until the logic gets big.</p>
<p>At that point, the code often turns into a web of transitions:</p>
<ul>
<li>if enemy seen, attack</li>
<li>if out of ammo, reload</li>
<li>if low health, flee</li>
<li>if no weapon, search</li>
<li>if cover available, move there</li>
<li>if target lost, search again</li>
</ul>
<p>Very quickly, the AI is not “a machine with states” anymore. It is a pile of exceptions.</p>
<p>GOAP is useful because it changes the shape of the problem. You stop asking, “What state should this AI be in?” and start asking, “What goal does this agent want, and what sequence of actions can get there?”</p>
<p>That is often a better fit for enemies, allies, and NPCs that need to react to changing conditions.</p>
<h2 id="the-pieces-of-a-goap-system">The pieces of a GOAP system</h2>
<p>A GOAP setup usually has three parts:</p>
<h3 id="1-world-state">1. World state</h3>
<p>This is a set of facts about the agent or the environment.</p>
<p>Examples:</p>
<ul>
<li>has weapon</li>
<li>has ammo</li>
<li>sees player</li>
<li>is in cover</li>
<li>health low</li>
<li>target in range</li>
</ul>
<p>You can store these as booleans, numbers, enums, or key-value pairs. The important part is that they are queryable and small enough to search efficiently.</p>
<h3 id="2-actions">2. Actions</h3>
<p>Each action describes:</p>
<ul>
<li><strong>preconditions</strong>: what must be true before the action can run</li>
<li><strong>effects</strong>: what becomes true after the action succeeds</li>
<li><strong>cost</strong>: how expensive the action is</li>
<li><strong>implementation</strong>: the actual gameplay behavior</li>
</ul>
<p>Examples:</p>
<ul>
<li><code>FindWeapon</code></li>
<li><code>Reload</code></li>
<li><code>MoveToCover</code></li>
<li><code>AttackTarget</code></li>
<li><code>Heal</code></li>
</ul>
<p>An action is not just animation. It is a gameplay step with meaning in the world model.</p>
<h3 id="3-goals">3. Goals</h3>
<p>A goal is a desired world state.</p>
<p>Examples:</p>
<ul>
<li>target dead</li>
<li>self healed</li>
<li>in cover</li>
<li>weapon acquired</li>
</ul>
<p>The planner looks for a chain of actions that can make the goal true from the current state.</p>
<h2 id="how-planning-works">How planning works</h2>
<p>Here is the simple version.</p>
<p>If the current state already satisfies the goal, no plan is needed.</p>
<p>If not, the planner searches backward or forward through actions to find a valid sequence.</p>
<p>For example:</p>
<ul>
<li>Goal: <code>target dead</code></li>
<li>Available action: <code>attack target</code></li>
<li>Precondition for attack: <code>has weapon</code> and <code>target in range</code></li>
<li>Available action: <code>move to target</code></li>
<li>Precondition for move: <code>can path to target</code></li>
</ul>
<p>Now the planner can build a chain:</p>
<ol>
<li>move to target</li>
<li>attack target</li>
</ol>
<p>If <code>has weapon</code> is false, it may add:</p>
<ol>
<li>find weapon</li>
<li>move to target</li>
<li>attack target</li>
</ol>
<p>The AI does not need a special “find weapon then attack” state. It discovers that sequence when the world demands it.</p>
<h2 id="why-goap-feels-flexible">Why GOAP feels flexible</h2>
<p>GOAP shines when the same action can serve multiple goals.</p>
<p>For example:</p>
<ul>
<li><code>MoveToCover</code> might help survival</li>
<li><code>MoveToTarget</code> might help attack</li>
<li><code>MoveToItem</code> might help resupply</li>
</ul>
<p>Because actions are reusable, you often get more behavior from fewer building blocks.</p>
<p>That makes GOAP attractive for emergent AI. It can produce behavior that looks intentional without requiring a giant hand-authored tree of every possible case.</p>
<h2 id="where-goap-is-not-magic">Where GOAP is not magic</h2>
<p>GOAP is not free.</p>
<p>It comes with costs:</p>
<ul>
<li>planning takes CPU</li>
<li>debugging can be harder than with a simple FSM</li>
<li>action design has to be clean</li>
<li>bad world state modeling leads to weird plans</li>
</ul>
<p>If your actions are too broad, the planner gets vague. If they are too narrow, you end up with too many tiny steps. Both extremes make the system harder to use.</p>
<p>You also need to think about interruption.</p>
<p>A plan may be valid now and invalid two seconds later. The player moves. The door closes. The target dies. The AI must replan when the world changes enough to matter.</p>
<h2 id="practical-implementation-advice">Practical implementation advice</h2>
<p>If you are building GOAP in Unity, start small.</p>
<p>A good first version usually has:</p>
<ul>
<li>a compact world state representation</li>
<li>a simple action base class</li>
<li>a planner that searches for the lowest-cost valid sequence</li>
<li>a blackboard or shared state object</li>
<li>replan triggers for invalidated goals</li>
</ul>
<p>Keep the action list short at first. Get one NPC working before you add ten goals and twenty actions.</p>
<p>A common pattern is to separate:</p>
<ul>
<li><strong>decision logic</strong>: the planner</li>
<li><strong>execution logic</strong>: the actual action running in the scene</li>
</ul>
<p>That separation keeps your system easier to test.</p>
<p>When those layers need shared facts, a blackboard is usually the cleanest place to put them. A separate note on that pattern lives here: <a href="/posts/blackboard-architecture-for-game-ai/">Blackboard Architecture for Game AI</a>.</p>
<h2 id="good-use-cases">Good use cases</h2>
<p>GOAP is a strong fit for:</p>
<ul>
<li>enemy AI that has to switch between fighting, retreating, and resupplying</li>
<li>survival NPCs</li>
<li>simulation-heavy games</li>
<li>agents with multiple competing needs</li>
</ul>
<p>It is less useful when the behavior is mostly cinematic, highly scripted, or very simple.</p>
<h2 id="practical-takeaway">Practical takeaway</h2>
<p>GOAP is useful because it makes AI think in terms of goals and actions instead of hardwired state transitions.</p>
<p>That does not make it automatically better than every other approach. It just means it scales well when the agent needs to choose among many valid action sequences.</p>
<p>If your AI needs to look adaptable, GOAP is worth the effort.</p>
<p>For a typed model-assisted way to choose which goal to hand to the planner, see <a href="/posts/using-jev-for-game-ai-typed-decisions-for-goap/">Using Jev for Game AI: Typed Decisions for GOAP</a>.</p>
<h2 id="diagram-note">Diagram note</h2>
<p>The flow diagram above is enough for this article. If you add a second visual later, show one concrete action chain or a small planner search tree.</p>
]]></content>
        </item>
        
    </channel>
</rss>
