<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: GameDevToolLab</title>
    <description>The latest articles on DEV Community by GameDevToolLab (@gamedevtoollab).</description>
    <link>https://dev.to/gamedevtoollab</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3992439%2F58833102-eb15-4c51-a035-e55b9d236f1e.png</url>
      <title>DEV Community: GameDevToolLab</title>
      <link>https://dev.to/gamedevtoollab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gamedevtoollab"/>
    <language>en</language>
    <item>
      <title>Does Unity Really Need DI? Keeping Zenject, Choosing Reflex, and Comparing VContainer</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Mon, 03 Aug 2026 11:33:13 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/does-unity-really-need-di-keeping-zenject-choosing-reflex-and-comparing-vcontainer-2ke5</link>
      <guid>https://dev.to/gamedevtoollab/does-unity-really-need-di-keeping-zenject-choosing-reflex-and-comparing-vcontainer-2ke5</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;As a Unity project grows, dependencies become harder to see and replace. Typical signs are more &lt;code&gt;GameManager.Instance&lt;/code&gt; calls, repeated scene lookups, and input, networking, save, analytics, or billing code that cannot be swapped without changing its callers.&lt;/p&gt;

&lt;p&gt;Dependency injection, or DI, addresses these problems by moving construction and lifetime management to explicit composition boundaries.&lt;/p&gt;

&lt;p&gt;Zenject and its fork Extenject were the best-known Unity DI containers for years. As of July 26, 2026, the latest &lt;a href="https://github.com/modesttree/Zenject/releases" rel="noopener noreferrer"&gt;Zenject release&lt;/a&gt; is 9.2.0 from May 2020, while &lt;a href="https://github.com/Mathijs-Bakker/Extenject/releases" rel="noopener noreferrer"&gt;Extenject&lt;/a&gt; has been quiet since its 9.3.1 prerelease in July 2022.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/gustavopsantos/reflex/releases" rel="noopener noreferrer"&gt;Reflex 14.3.1&lt;/a&gt; was released in June 2026 and &lt;a href="https://github.com/hadashiA/VContainer/releases" rel="noopener noreferrer"&gt;VContainer 1.19.0&lt;/a&gt; in July 2026. This article uses Unity 6-era APIs and describes the libraries as they existed on July 26, 2026.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keep Zenject or Extenject in an existing project if it is stable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For a new project, evaluate Reflex first.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compare VContainer when you need its broader lifecycle features; use manual DI when the project is small.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Zenject is not suddenly unusable. The concern is that a DI container affects scene startup, prefab creation, lifetimes, editor tooling, generated code, and IL2CPP builds. For a new multi-year project, maintenance matters as much as convenience.&lt;/p&gt;

&lt;h2&gt;
  
  
  DI is not the same as using interfaces
&lt;/h2&gt;

&lt;p&gt;DI means that an object receives dependencies instead of constructing them internally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerMoveService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IPlayerInput&lt;/span&gt; &lt;span class="n"&gt;_input&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;PlayerMoveService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IPlayerInput&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_input&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The composition root can create &lt;code&gt;IPlayerInput&lt;/code&gt; and pass it to &lt;code&gt;PlayerMoveService&lt;/code&gt;. This is already DI; a container is optional.&lt;/p&gt;

&lt;p&gt;Manual construction is often clearer for a small graph. A container becomes useful when the project needs consistent registration, scopes, disposal, and scene-aware object creation. DI should expose dependencies, not hide them behind a global resolver.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Unity makes DI more complicated
&lt;/h2&gt;

&lt;p&gt;Plain C# objects can use constructor injection. Unity adds &lt;code&gt;MonoBehaviour&lt;/code&gt;, scenes, prefabs, Inspector references, and callback order.&lt;/p&gt;

&lt;p&gt;A practical split is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plain C# classes:&lt;/strong&gt; constructor injection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scene &lt;code&gt;MonoBehaviour&lt;/code&gt; components:&lt;/strong&gt; method or field injection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;References local to one prefab:&lt;/strong&gt; &lt;code&gt;SerializeField&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime-created prefabs:&lt;/strong&gt; a project-owned factory plus explicit injection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not move every reference into DI. Buttons, labels, and child transforms are usually clearer in the Inspector. DI is most useful for networking, save data, input, time, randomness, purchases, logging, and analytics.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Awake&lt;/code&gt; and dynamic prefabs
&lt;/h3&gt;

&lt;p&gt;For ordinary scene objects, Reflex intends to inject before &lt;code&gt;Awake&lt;/code&gt; and &lt;code&gt;OnEnable&lt;/code&gt; when a &lt;code&gt;ContainerScope&lt;/code&gt; exists. Do not assume the same behavior for altered &lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/execution-order.html" rel="noopener noreferrer"&gt;script execution order&lt;/a&gt;, additive or asynchronous loading, or manually instantiated prefabs.&lt;/p&gt;

&lt;p&gt;An active prefab runs &lt;code&gt;Awake&lt;/code&gt; and &lt;code&gt;OnEnable&lt;/code&gt; during &lt;code&gt;Instantiate&lt;/code&gt;; later injection cannot change that. A safe project-wide rule is to keep &lt;code&gt;Awake&lt;/code&gt; self-contained and begin DI-dependent work from &lt;code&gt;Initialize&lt;/code&gt; or &lt;code&gt;StartAsync&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model lifetimes explicitly
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Singleton:&lt;/strong&gt; one shared instance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoped:&lt;/strong&gt; one instance per container&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transient:&lt;/strong&gt; one instance per resolution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;API clients may belong in the root scope. Battle score, waves, and enemy collections usually belong to a scene scope. DI is valuable because it makes lifetime and disposal boundaries explicit, not because it creates more singletons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Zenject became popular
&lt;/h2&gt;

&lt;p&gt;Zenject addressed Unity-specific problems through contexts, installers, initialization and tick interfaces, factories, pools, &lt;code&gt;SignalBus&lt;/code&gt;, validation, subcontainers, and prefab injection. Its feature set and documentation made it a natural default.&lt;/p&gt;

&lt;p&gt;The cost appears when application code directly depends on &lt;code&gt;DiContainer&lt;/code&gt;, &lt;code&gt;PlaceholderFactory&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;SignalBus&lt;/code&gt;, &lt;code&gt;ITickable&lt;/code&gt;, or &lt;code&gt;IInitializable&lt;/code&gt;. Replacing the container then requires redesigning factories, events, update loops, and startup order.&lt;/p&gt;

&lt;p&gt;Keep container-specific conveniences near the composition boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is Zenject still usable?
&lt;/h2&gt;

&lt;p&gt;Yes, especially in an existing project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Existing projects
&lt;/h3&gt;

&lt;p&gt;Do not rewrite a stable foundation only because upstream development is quiet. Migration can affect scene startup, prefab ownership, factories, pools, signals, tests, Addressables, and IL2CPP stripping.&lt;/p&gt;

&lt;p&gt;Keeping Zenject is reasonable when current builds are continuously tested, the team can patch or fork it, and the project already relies on Zenject-specific features. “Unmaintained” should trigger risk assessment, not an automatic rewrite.&lt;/p&gt;

&lt;h3&gt;
  
  
  New projects
&lt;/h3&gt;

&lt;p&gt;The justification is weaker. Unity has continued to change editor APIs, IL2CPP, code generation, and Unity 6 internals since the last main Zenject release. Reflex and VContainer have shipped recent compatibility updates.&lt;/p&gt;

&lt;p&gt;A DI package touches scenes, generated code, reflection, editor tools, and build pipelines. Choosing Zenject for a new multi-year project means accepting that your team may become its effective maintainer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is Zenject too slow?
&lt;/h2&gt;

&lt;p&gt;The Reflex README publishes a benchmark that resolves a transient object with a four-level dependency chain 10,000 times.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;Reflex&lt;/th&gt;
&lt;th&gt;Zenject&lt;/th&gt;
&lt;th&gt;VContainer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Android / Mono&lt;/td&gt;
&lt;td&gt;4.9 ms / 54.7 KB&lt;/td&gt;
&lt;td&gt;34.4 ms / 503.9 KB&lt;/td&gt;
&lt;td&gt;20.3 ms / 70.3 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android / IL2CPP&lt;/td&gt;
&lt;td&gt;4.0 ms / 140.6 KB&lt;/td&gt;
&lt;td&gt;15.8 ms / 1,000 KB&lt;/td&gt;
&lt;td&gt;4.2 ms / 140.6 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows / Mono&lt;/td&gt;
&lt;td&gt;0.7 ms / 140.6 KB&lt;/td&gt;
&lt;td&gt;5.6 ms / 1,000 KB&lt;/td&gt;
&lt;td&gt;1.9 ms / 140.6 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows / IL2CPP&lt;/td&gt;
&lt;td&gt;1.4 ms / 140.6 KB&lt;/td&gt;
&lt;td&gt;6.2 ms / 1,000 KB&lt;/td&gt;
&lt;td&gt;3.0 ms / 140.6 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each cell is &lt;strong&gt;time / GC allocation&lt;/strong&gt;. See the current &lt;a href="https://github.com/gustavopsantos/reflex#-performance" rel="noopener noreferrer"&gt;Reflex performance table&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is a Reflex-published benchmark, not an independent test. It measures resolution throughput, not scene scanning, initial container construction, player size, or a complete startup path.&lt;/p&gt;

&lt;p&gt;Within this workload, Reflex is faster and allocates less than Zenject. That may matter when resolving many transient objects at startup, but it does not make Zenject unusable. DI should normally resolve at composition boundaries, while gameplay uses already-injected references.&lt;/p&gt;

&lt;p&gt;If a project resolves thousands of dependencies every frame, redesigning that path matters more than replacing the container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Reflex is a strong default for new projects
&lt;/h2&gt;

&lt;p&gt;Reflex provides a relatively small Unity-focused DI surface instead of reproducing every Zenject feature. As of July 2026 it includes root and scene containers, three lifetimes, lazy and eager resolution, multiple injection styles, factories, manual scopes, runtime GameObject injection, a debugger, and a Roslyn Source Generator.&lt;/p&gt;

&lt;p&gt;It does not depend on runtime code emission and targets IL2CPP and WebGL. Version 14.3.1 also continued Unity 6 compatibility work.&lt;/p&gt;

&lt;p&gt;Recent releases do not guarantee future health. Inspect issue response, contributor concentration, community knowledge, and whether your team can diagnose failures without upstream help.&lt;/p&gt;

&lt;p&gt;Keep application-facing factories and events behind project-owned abstractions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IEnemyFactory&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;EnemyView&lt;/span&gt; &lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EnemySpawnParameter&lt;/span&gt; &lt;span class="n"&gt;parameter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the composition root should know about Reflex's &lt;code&gt;Container&lt;/code&gt; or &lt;code&gt;RegisterFactory&lt;/code&gt;. The DI package should assemble the architecture, not become it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Reflex
&lt;/h2&gt;

&lt;p&gt;This article uses the &lt;strong&gt;Reflex 14.x API&lt;/strong&gt;. Older examples may use &lt;code&gt;AddSingleton&lt;/code&gt;, &lt;code&gt;AddScoped&lt;/code&gt;, or &lt;code&gt;AddTransient&lt;/code&gt;, so check the package version.&lt;/p&gt;

&lt;p&gt;In Unity Package Manager, select &lt;strong&gt;Add package from git URL&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/gustavopsantos/reflex.git?path=/Assets/Reflex/#14.3.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pin a release tag and check &lt;a href="https://github.com/gustavopsantos/reflex/releases" rel="noopener noreferrer"&gt;Reflex Releases&lt;/a&gt; before adoption.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;ReflexSettings&lt;/code&gt; directly under &lt;code&gt;Assets/Resources&lt;/code&gt; with &lt;code&gt;Create &amp;gt; Reflex &amp;gt; Settings&lt;/code&gt;. For application-wide registrations, create a RootScope prefab and add it to those settings. For each DI-enabled scene, create &lt;code&gt;GameObject &amp;gt; Reflex &amp;gt; SceneScope&lt;/code&gt;, which adds a &lt;code&gt;ContainerScope&lt;/code&gt; component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registering plain C# classes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IPlayerInput&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Vector2&lt;/span&gt; &lt;span class="nf"&gt;ReadMove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UnityPlayerInput&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IPlayerInput&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Vector2&lt;/span&gt; &lt;span class="nf"&gt;ReadMove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAxisRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Horizontal"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAxisRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Vertical"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerMoveService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IPlayerInput&lt;/span&gt; &lt;span class="n"&gt;_input&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;PlayerMoveService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IPlayerInput&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_input&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="nf"&gt;CalculateVelocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Vector2&lt;/span&gt; &lt;span class="n"&gt;move&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadMove&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service does not know the Unity input API and can use a test implementation of &lt;code&gt;IPlayerInput&lt;/code&gt;. The sample uses the legacy Input Manager only to stay small.&lt;/p&gt;

&lt;p&gt;Register it with the &lt;a href="https://github.com/gustavopsantos/reflex#-bindings" rel="noopener noreferrer"&gt;Reflex 14.x bindings API&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Reflex.Core&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Reflex.Enums&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GameplayInstaller&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IInstaller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;InstallBindings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ContainerBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UnityPlayerInput&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IPlayerInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;Lifetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Singleton&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Resolution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lazy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerMoveService&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerMoveService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;Lifetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scoped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Resolution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lazy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Lifetime.Singleton&lt;/code&gt;: one instance shared by the registering container and its children&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Lifetime.Scoped&lt;/code&gt;: one instance per container&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Lifetime.Transient&lt;/code&gt;: a new instance per resolution&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Resolution.Lazy&lt;/code&gt;: create on first use&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Resolution.Eager&lt;/code&gt;: create while building the container&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Injecting a &lt;code&gt;MonoBehaviour&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Reflex.Attributes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerView&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;CharacterController&lt;/span&gt; &lt;span class="n"&gt;_controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;_speed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;PlayerMoveService&lt;/span&gt; &lt;span class="n"&gt;_moveService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerMoveService&lt;/span&gt; &lt;span class="n"&gt;moveService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_moveService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;moveService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="n"&gt;velocity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_moveService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CalculateVelocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_speed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;velocity&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deltaTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller and speed are prefab-local configuration, so they stay in the Inspector. The input-dependent service is injected.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;Construct&lt;/code&gt; is not called, check that the scene contains a SceneScope or &lt;code&gt;ContainerScope&lt;/code&gt;, the scene was loaded through the expected path, and &lt;code&gt;PlayerMoveService&lt;/code&gt; is registered in that scene or a parent scope.&lt;/p&gt;

&lt;p&gt;For production code, validate injection during explicit initialization before the view joins the update loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registering a &lt;code&gt;ScriptableObject&lt;/code&gt; or existing instance
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;RegisterValue&lt;/code&gt; for an existing settings asset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CreateAssetMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;menuName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Game/Player Move Settings"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerMoveSettings&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ScriptableObject&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;Speed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GameplayInstaller&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IInstaller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;PlayerMoveSettings&lt;/span&gt; &lt;span class="n"&gt;_settings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;InstallBindings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ContainerBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;_settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerMoveSettings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RegisterValue&lt;/code&gt; creates a singleton registration. If the value implements &lt;code&gt;IDisposable&lt;/code&gt;, Reflex disposes it with the container, so avoid duplicate ownership by another SDK or scope.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Dispose&lt;/code&gt; is not &lt;code&gt;UnityEngine.Object.Destroy&lt;/code&gt;; Unity objects still follow scene, prefab, and explicit Unity ownership rules.&lt;/p&gt;

&lt;p&gt;Avoid many raw string or numeric registrations. Reflex has no direct &lt;code&gt;WithId&lt;/code&gt; equivalent, so use small types such as &lt;code&gt;ApiBaseUrl&lt;/code&gt; and &lt;code&gt;AssetBaseUrl&lt;/code&gt; to prevent accidental wiring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not turn &lt;code&gt;Resolve&lt;/code&gt; into a Service Locator
&lt;/h2&gt;

&lt;p&gt;Passing &lt;code&gt;Container&lt;/code&gt; through application code and calling &lt;code&gt;Resolve&amp;lt;T&amp;gt;&lt;/code&gt; hides the class's real dependencies and forces tests to construct a container.&lt;/p&gt;

&lt;p&gt;Inject the services directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BattleService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ISaveDataRepository&lt;/span&gt; &lt;span class="n"&gt;_saveData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IAnalytics&lt;/span&gt; &lt;span class="n"&gt;_analytics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;BattleService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ISaveDataRepository&lt;/span&gt; &lt;span class="n"&gt;saveData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IAnalytics&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_saveData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;saveData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_analytics&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Limit container access to composition roots, installers, DI-aware factories, and framework boundaries. Microsoft's &lt;a href="https://learn.microsoft.com/dotnet/core/extensions/dependency-injection/guidelines" rel="noopener noreferrer"&gt;DI guidelines&lt;/a&gt; recommend the same approach.&lt;/p&gt;

&lt;p&gt;When explicit resolution is necessary, &lt;code&gt;Single&amp;lt;T&amp;gt;&lt;/code&gt; detects duplicate registrations. &lt;code&gt;Resolve&amp;lt;T&amp;gt;&lt;/code&gt; returns the last valid registration. Use &lt;code&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/code&gt; or distinct contracts when multiple implementations are intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source Generator and IL2CPP
&lt;/h2&gt;

&lt;p&gt;Reflex supports IL2CPP without runtime code emission and provides a Roslyn Source Generator.&lt;/p&gt;

&lt;p&gt;For generated field, property, or method injection, add &lt;a href="https://github.com/gustavopsantos/reflex#sourcegeneratorinjectable" rel="noopener noreferrer"&gt;&lt;code&gt;SourceGeneratorInjectable&lt;/code&gt;&lt;/a&gt; and make the type &lt;code&gt;public partial&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SourceGeneratorInjectable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerView&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerMoveService&lt;/span&gt; &lt;span class="n"&gt;moveService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_moveService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;moveService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For nested types, every containing type must also be &lt;code&gt;public partial&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Source generation does not remove every AOT limitation. The Reflex README notes a possible &lt;code&gt;ExecutionEngineException&lt;/code&gt; for constructor-injected &lt;code&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/code&gt; when required IL2CPP code is missing.&lt;/p&gt;

&lt;p&gt;Test early with desktop and Android IL2CPP, production stripping settings, iOS Xcode generation when relevant, and Addressables-created scenes and prefabs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating dynamic prefabs safely
&lt;/h2&gt;

&lt;p&gt;Scene objects can be injected by SceneScope. Runtime prefabs should go through one project-owned factory. Reflex provides &lt;code&gt;GameObjectSelfInjector&lt;/code&gt; and the &lt;code&gt;GameObjectInjector&lt;/code&gt; APIs.&lt;/p&gt;

&lt;p&gt;The exact implementation changes with Addressables and pooling, so this is conceptual code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EnemyViewFactory&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEnemyViewFactory&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;EnemyView&lt;/span&gt; &lt;span class="n"&gt;_prefab&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Transform&lt;/span&gt; &lt;span class="n"&gt;_parent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;EnemyViewFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EnemyView&lt;/span&gt; &lt;span class="n"&gt;prefab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Transform&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_prefab&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prefab&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_parent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;EnemyView&lt;/span&gt; &lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;EnemyView&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Instantiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;_prefab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Quaternion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_parent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Container&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;_parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSceneContainer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;GameObjectInjector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InjectRecursive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep container-specific types inside the factory and choose the container that represents the object's actual scene or child scope.&lt;/p&gt;

&lt;p&gt;Recommended rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;do not use DI dependencies in &lt;code&gt;Awake&lt;/code&gt; or &lt;code&gt;OnEnable&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;keep &lt;code&gt;create -&amp;gt; inject -&amp;gt; initialize -&amp;gt; join gameplay&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;inject pooled instances once and reset them on reuse,&lt;/li&gt;
&lt;li&gt;reconsider pool sharing when the parent scope changes,&lt;/li&gt;
&lt;li&gt;keep Addressables handles and release responsibility in the factory or pool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;SetActive(false)&lt;/code&gt; after creation cannot undo callbacks that already ran, and &lt;code&gt;GameObjectSelfInjector&lt;/code&gt; cannot guarantee every component's relative &lt;code&gt;Awake&lt;/code&gt; order.&lt;/p&gt;

&lt;h2&gt;
  
  
  VContainer is the main alternative
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/hadashiA/VContainer/releases" rel="noopener noreferrer"&gt;VContainer 1.19.0&lt;/a&gt; remains actively maintained. It provides &lt;code&gt;LifetimeScope&lt;/code&gt;, source generation, PlayerLoop integration, pure C# entry points, UniTask and ECS integration, diagnostics, async-scene child scopes, and keyed registration.&lt;/p&gt;

&lt;p&gt;These features matter when DI should coordinate async startup, Entities, or a pure C# update loop. They are not requirements for every GameObject project.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision point&lt;/th&gt;
&lt;th&gt;Reflex&lt;/th&gt;
&lt;th&gt;VContainer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direction&lt;/td&gt;
&lt;td&gt;Small Unity-focused DI surface&lt;/td&gt;
&lt;td&gt;DI plus lifecycle and entry-point integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scene injection&lt;/td&gt;
&lt;td&gt;SceneScope injects the scene&lt;/td&gt;
&lt;td&gt;Explicit component registration is central&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pure C# entry points&lt;/td&gt;
&lt;td&gt;Project-owned when needed&lt;/td&gt;
&lt;td&gt;Interfaces such as &lt;code&gt;IStartable&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UniTask integration&lt;/td&gt;
&lt;td&gt;Separate from DI core&lt;/td&gt;
&lt;td&gt;Official integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyed registration&lt;/td&gt;
&lt;td&gt;Use distinct types&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the Reflex-published Android IL2CPP benchmark, Reflex records 4.0 ms and VContainer 4.2 ms. The difference is too small to choose by ranking alone.&lt;/p&gt;

&lt;p&gt;Compare registration style, PlayerLoop requirements, lifetime design, team experience, and debugging needs in a small prototype.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual DI is also valid
&lt;/h2&gt;

&lt;p&gt;For a small game or isolated feature, direct construction may be safer than adding a container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UnityPlayerInput&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;moveService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PlayerMoveService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_playerView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;moveService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is only a minimal composition example. A real project can place async startup in a separate bootstrap flow.&lt;/p&gt;

&lt;p&gt;Manual DI is easy to inspect, has no container-specific AOT behavior, and is unaffected by third-party maintenance. Add a container when the dependency graph, scopes, or disposal requirements justify it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping Zenject concepts to Reflex
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Zenject / Extenject&lt;/th&gt;
&lt;th&gt;Reflex or project-owned replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ProjectContext&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RootScope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SceneContext&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scene &lt;code&gt;ContainerScope&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MonoInstaller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MonoBehaviour, IInstaller&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BindInstance&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RegisterValue&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;AsSingle&lt;/code&gt; / &lt;code&gt;AsTransient&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Singleton / Transient lifetime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AsCached&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Re-evaluate as scoped or singleton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NonLazy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Resolution.Eager&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PlaceholderFactory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Project factory plus &lt;code&gt;RegisterFactory&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SignalBus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;C# events, R3, or a project EventBus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MemoryPool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;UnityEngine.Pool&lt;/code&gt; or a project pool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;IInitializable&lt;/code&gt; / &lt;code&gt;ITickable&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Explicit bootstrap and project update loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SubContainer&lt;/td&gt;
&lt;td&gt;Child scopes or scene hierarchy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not mechanically rename APIs. Lifetimes, subscription disposal, and update order must be reconsidered.&lt;/p&gt;

&lt;p&gt;A safer migration order is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stop adding new Zenject-specific types.&lt;/li&gt;
&lt;li&gt;Wrap factories and events in project-owned interfaces.&lt;/li&gt;
&lt;li&gt;Remove Zenject references from plain C# domain code.&lt;/li&gt;
&lt;li&gt;Separate composition roots by scene or feature.&lt;/li&gt;
&lt;li&gt;Add startup and disposal tests.&lt;/li&gt;
&lt;li&gt;Replace installers and contexts last.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Avoid long-term coexistence and never let both containers create the same singleton.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common DI failures in Unity
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Creating an interface for everything
&lt;/h3&gt;

&lt;p&gt;Not every value object or calculation class needs an &lt;code&gt;IFoo&lt;/code&gt;. Prioritize external I/O, time, randomness, input, networking, storage, purchases, and analytics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using the container as a global variable
&lt;/h3&gt;

&lt;p&gt;Calling &lt;code&gt;Container.Resolve&amp;lt;T&amp;gt;()&lt;/code&gt; everywhere hides dependencies and makes tests require a configured container.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resolving every frame
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IPlayerInput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inject once and keep the reference. Fixing this design usually matters more than choosing a faster container.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storing scene state in root singletons
&lt;/h3&gt;

&lt;p&gt;Root scope suits APIs, authentication, master data, logging, and analytics. Battle state and screen-specific presenters normally belong to a scene scope.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expecting DI to define async startup order
&lt;/h3&gt;

&lt;p&gt;DI resolves dependencies; it does not define the correct order for loading master data, save files, and scenes. Use an explicit bootstrap flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;DI is not about eliminating every singleton or creating an interface for every class. It exposes dependencies, centralizes composition, and makes root, scene, and prefab lifetimes explicit.&lt;/p&gt;

&lt;p&gt;Zenject can remain a reasonable foundation for a stable existing project. Its latest main release dates from 2020, however, and Extenject has also been quiet since 2022. For a new project that must follow future Unity versions, it is difficult to justify Zenject as the default.&lt;/p&gt;

&lt;p&gt;Reflex remains actively maintained and provides Unity-focused scopes, source generation, debugging, and runtime prefab injection. VContainer offers a broader lifecycle and entry-point model when a project needs it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keep an existing Zenject project when migration cost is not justified.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluate Reflex first for a new Unity project.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compare VContainer when PlayerLoop integration, pure C# entry points, UniTask, ECS, or broader lifecycle management are real requirements.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with manual DI when the project is small.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A DI container automates construction and lifetime management; it does not design the software. Keep it near composition boundaries and preserve the option to replace it later.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/gustavopsantos/reflex" rel="noopener noreferrer"&gt;Reflex GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gustavopsantos/reflex/releases" rel="noopener noreferrer"&gt;Reflex Releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vcontainer.hadashikick.jp/" rel="noopener noreferrer"&gt;VContainer documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/hadashiA/VContainer" rel="noopener noreferrer"&gt;VContainer GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/hadashiA/VContainer/releases" rel="noopener noreferrer"&gt;VContainer Releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/modesttree/Zenject/releases" rel="noopener noreferrer"&gt;Zenject Releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Mathijs-Bakker/Extenject/releases" rel="noopener noreferrer"&gt;Extenject Releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/dotnet/core/extensions/dependency-injection/guidelines" rel="noopener noreferrer"&gt;Microsoft: Dependency injection guidelines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/class-MonoBehaviour.html" rel="noopener noreferrer"&gt;Unity Manual: MonoBehaviour&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/execution-order.html" rel="noopener noreferrer"&gt;Unity Manual: Order of execution for event functions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>gamedev</category>
      <category>dependencyinjection</category>
    </item>
    <item>
      <title>Testing in Unreal Engine 5: CQTest, Automation Spec, Functional Tests, and CI</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Fri, 31 Jul 2026 05:21:19 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/testing-in-unreal-engine-5-cqtest-automation-spec-functional-tests-and-ci-3776</link>
      <guid>https://dev.to/gamedevtoollab/testing-in-unreal-engine-5-cqtest-automation-spec-functional-tests-and-ci-3776</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The hardest part of writing tests in Unreal Engine 5 is often not the assertion syntax. It is deciding which testing system to use.&lt;/p&gt;

&lt;p&gt;UE provides several overlapping options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IMPLEMENT_SIMPLE_AUTOMATION_TEST&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Complex Automation Tests&lt;/li&gt;
&lt;li&gt;Automation Spec&lt;/li&gt;
&lt;li&gt;CQTest&lt;/li&gt;
&lt;li&gt;Functional Tests&lt;/li&gt;
&lt;li&gt;Python Automation Tests&lt;/li&gt;
&lt;li&gt;Automation Driver&lt;/li&gt;
&lt;li&gt;Gauntlet&lt;/li&gt;
&lt;li&gt;Low-Level Tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Games also require very different execution environments. Some rules can be tested as ordinary C++ functions. Others require &lt;code&gt;UObject&lt;/code&gt;, &lt;code&gt;UWorld&lt;/code&gt;, Actors, PIE, level streaming, UI input, rendering, network sessions, or a packaged build.&lt;/p&gt;

&lt;p&gt;Trying to force all of those cases into one framework usually creates a slow and fragile test suite.&lt;/p&gt;

&lt;p&gt;This article uses Unreal Engine 5.8 documentation as its baseline and explains how to divide responsibilities across C++ tests, CQTest, Automation Spec, asynchronous waits, Functional Tests, command-line execution, and CI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Code sample scope:&lt;/strong&gt; The snippets reference UE 5.8 APIs, but they are explanatory fragments rather than a complete sample project. Project-specific types, includes, &lt;code&gt;.Build.cs&lt;/code&gt; dependencies, module API macros, and some setup code must be adapted to your project. The examples were not fully compiled in a UE 5.8 project while preparing this article.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The practical rule is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Use the lightest execution environment that can prove the behavior you care about.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Which UE Test System Should You Use?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Good first choice&lt;/th&gt;
&lt;th&gt;Typical environment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Math, rules, and data conversion&lt;/td&gt;
&lt;td&gt;CQTest, Automation Spec, or Simple Automation Test&lt;/td&gt;
&lt;td&gt;Editor / command line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behavior expressed as readable scenarios&lt;/td&gt;
&lt;td&gt;Automation Spec&lt;/td&gt;
&lt;td&gt;Editor / command line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The same logic with many inputs&lt;/td&gt;
&lt;td&gt;Complex Automation Test&lt;/td&gt;
&lt;td&gt;Editor / command line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actors, Components, Blueprints, and level wiring&lt;/td&gt;
&lt;td&gt;Functional Test&lt;/td&gt;
&lt;td&gt;Editor / PIE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asset and content-production rules&lt;/td&gt;
&lt;td&gt;Python Automation Test&lt;/td&gt;
&lt;td&gt;Editor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mouse, keyboard, scrolling, and UI flows&lt;/td&gt;
&lt;td&gt;Automation Driver&lt;/td&gt;
&lt;td&gt;Editor / Client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packaged builds, devices, clients, and servers&lt;/td&gt;
&lt;td&gt;Gauntlet&lt;/td&gt;
&lt;td&gt;Build machine / device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast tests in a dedicated executable&lt;/td&gt;
&lt;td&gt;Low-Level Tests&lt;/td&gt;
&lt;td&gt;Dedicated test target&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not introduce everything at once. Keep most rule verification in fast C++ tests, add a small number of Functional Tests for critical gameplay wiring, and introduce Gauntlet only when the packaged runtime or multi-process environment is part of the requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate Game Rules from Actors and Widgets
&lt;/h2&gt;

&lt;p&gt;The most effective preparation for testing is to move deterministic rules out of Actors and Widgets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DamageCalculator.h&lt;/span&gt;
&lt;span class="cp"&gt;#pragma once
&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"CoreMinimal.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FDamageCalculator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nl"&gt;public:&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;CalculateFinalDamage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;BaseDamage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;Defense&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseDamage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;FMath&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BaseDamage&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;FMath&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Defense&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function can be checked without creating a World or spawning an Actor. The gameplay Actor can read input values and apply the result, while a much smaller Functional Test verifies that the Unreal-side wiring is correct.&lt;/p&gt;

&lt;p&gt;The goal is not to remove every Unreal type from the codebase. The goal is to separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logic that benefits from many fast boundary-value tests&lt;/li&gt;
&lt;li&gt;Engine integration that requires a World, Actor, Component, Blueprint, or level&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to Put Test Code
&lt;/h2&gt;

&lt;p&gt;Epic's documentation shows Automation Tests near the related module, commonly under &lt;code&gt;Private/Tests&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source/MyGame/
├─ Public/Combat/DamageCalculator.h
└─ Private/
   ├─ Combat/DamageCalculator.cpp
   └─ Tests/DamageCalculatorTest.cpp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a reasonable starting point for small tests that only depend on modules such as &lt;code&gt;Core&lt;/code&gt; and &lt;code&gt;Engine&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, wrapping registration code with &lt;code&gt;WITH_DEV_AUTOMATION_TESTS&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; remove &lt;code&gt;.Build.cs&lt;/code&gt; dependencies. When tests begin to require &lt;code&gt;CQTest&lt;/code&gt;, editor-only modules, or Functional Testing support, move them into a dedicated module such as &lt;code&gt;Source/MyGameTests&lt;/code&gt; or a Tests plugin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source/MyGameTests/
├─ MyGameTests.Build.cs
└─ Private/
   ├─ Unit/
   └─ Functional/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The examples below omit repeated guards for readability. In real files, guard C++ test registration code—Automation Tests, Specs, and CQTest tests—with the appropriate development-test configuration. Keep test-only classes and dependencies out of Shipping targets according to your project's build policy.&lt;/p&gt;

&lt;p&gt;The sample names &lt;code&gt;FInventoryService&lt;/code&gt;, &lt;code&gt;FProfileService&lt;/code&gt;, &lt;code&gt;APickupActor&lt;/code&gt;, and &lt;code&gt;UInventoryComponent&lt;/code&gt; represent project-specific types. Replace them with your own implementations and dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simple Automation Tests
&lt;/h2&gt;

&lt;p&gt;A traditional Automation Test derives from &lt;code&gt;FAutomationTestBase&lt;/code&gt;. A single test can be registered with &lt;code&gt;IMPLEMENT_SIMPLE_AUTOMATION_TEST&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"Misc/AutomationTest.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Combat/DamageCalculator.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="cp"&gt;#if WITH_DEV_AUTOMATION_TESTS
&lt;/span&gt;
&lt;span class="n"&gt;IMPLEMENT_SIMPLE_AUTOMATION_TEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;FDamageCalculatorMinimumDamageTest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"MyGame.Unit.Combat.DamageCalculator.MinimumDamage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EAutomationTestFlags&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;EditorContext&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;EAutomationTestFlags&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ProductFilter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;FDamageCalculatorMinimumDamageTest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RunTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FString&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;Actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FDamageCalculator&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CalculateFinalDamage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;TestEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Positive attacks return at least one damage"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;Actual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RunTest&lt;/code&gt; can use helpers such as &lt;code&gt;TestEqual&lt;/code&gt;, &lt;code&gt;TestTrue&lt;/code&gt;, &lt;code&gt;TestFalse&lt;/code&gt;, and &lt;code&gt;TestNotNull&lt;/code&gt;. Stop early when a missing prerequisite would make later code unsafe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;UObject&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CreateObjectUnderTest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;TestNotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The object under test was created"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Treat Test Names as a CI Hierarchy
&lt;/h3&gt;

&lt;p&gt;The test name is both a display name and a command-line filter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MyGame.Unit.Combat.DamageCalculator.MinimumDamage
MyGame.Integration.Save.LoadRoundTrip
MyGame.Functional.Gameplay.Pickup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A pattern such as &lt;code&gt;Project.Layer.Feature.Subject.Condition&lt;/code&gt; makes it easy to run &lt;code&gt;MyGame.Unit&lt;/code&gt; before submission and reserve &lt;code&gt;MyGame.Functional&lt;/code&gt; for a heavier job.&lt;/p&gt;

&lt;p&gt;Common flags include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;EditorContext&lt;/code&gt;: Run in the Editor&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ClientContext&lt;/code&gt; / &lt;code&gt;ServerContext&lt;/code&gt;: Run in a client or server context&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SmokeFilter&lt;/code&gt;: Extremely short checks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ProductFilter&lt;/code&gt;: Project or product functionality&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PerfFilter&lt;/code&gt; / &lt;code&gt;StressFilter&lt;/code&gt;: Performance or stress testing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NonNullRHI&lt;/code&gt;: Requires a rendering environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;ProductFilter&lt;/code&gt; is normally appropriate for game-project tests, while &lt;code&gt;EngineFilter&lt;/code&gt; is intended for engine functionality. Epic's guidance treats Smoke Tests as checks that should complete in roughly one second or less.&lt;/p&gt;

&lt;h2&gt;
  
  
  CQTest for Fixture-Style C++ Tests
&lt;/h2&gt;

&lt;p&gt;CQTest extends the Automation Test infrastructure with fixtures, setup and teardown, assertions, and latent actions.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;CQTest&lt;/code&gt; to the module that builds the tests—not automatically to the product Runtime module.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;PrivateDependencyModuleNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Core"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"CoreUObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Engine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"CQTest"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A minimal test is concise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"CQTest.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Combat/DamageCalculator.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="n"&gt;TEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DamageCalculator_StandardReduction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"MyGame.Unit.Combat"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;Actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FDamageCalculator&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CalculateFinalDamage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;ASSERT_THAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AreEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Actual&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;TEST_CLASS&lt;/code&gt; when several tests share a fixture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;TEST_CLASS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InventoryServiceTest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"MyGame.Unit.Inventory"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TUniquePtr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FInventoryService&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;BEFORE_EACH&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Inventory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MakeUnique&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FInventoryService&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;AFTER_EACH&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;TEST_METHOD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AddItem_IncreasesCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ASSERT_THAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IsTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Inventory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Potion"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
        &lt;span class="n"&gt;ASSERT_THAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AreEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Inventory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetItemCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Potion"&lt;/span&gt;&lt;span class="p"&gt;))));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;TEST_METHOD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AddItem_RejectsCapacityOverflow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Inventory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Potion"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;ASSERT_THAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IsFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Inventory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Ether"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rebuilding the state before each method reduces order dependence. CQTest is a strong default for new fixture-style C++ tests, but an existing project that already uses Automation Spec consistently does not need to replace it only for stylistic reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automation Spec for Readable Behavior Scenarios
&lt;/h2&gt;

&lt;p&gt;Automation Spec uses a BDD-style structure with &lt;code&gt;Describe&lt;/code&gt;, &lt;code&gt;BeforeEach&lt;/code&gt;, and &lt;code&gt;It&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;BEGIN_DEFINE_SPEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;FInventoryServiceSpec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"MyGame.Unit.Inventory.InventoryService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EAutomationTestFlags&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;EditorContext&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;EAutomationTestFlags&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ProductFilter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;TUniquePtr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FInventoryService&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;END_DEFINE_SPEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FInventoryServiceSpec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;FInventoryServiceSpec&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Define&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;BeforeEach&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;]()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Inventory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MakeUnique&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FInventoryService&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;Describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"when adding an item to an empty inventory"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;]()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;It&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"increases the count by the added amount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;]()&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Inventory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Potion"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;TestEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Potion count"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;Inventory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetItemCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Potion"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Specs work well when the scenarios themselves should read like documentation. Epic recommends the &lt;code&gt;.spec.cpp&lt;/code&gt; suffix.&lt;/p&gt;

&lt;p&gt;Avoid deeply nested &lt;code&gt;Describe&lt;/code&gt; blocks. Two or three levels are usually enough; beyond that, the active setup becomes difficult to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complex Automation Tests for Parameter Sets
&lt;/h2&gt;

&lt;p&gt;A Complex Automation Test uses &lt;code&gt;GetTests&lt;/code&gt; to expose multiple cases and passes each command string to &lt;code&gt;RunTest&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"Misc/LexFromString.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="n"&gt;IMPLEMENT_COMPLEX_AUTOMATION_TEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;FDamageCalculatorCasesTest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"MyGame.Unit.Combat.DamageCalculator.Cases"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EAutomationTestFlags&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;EditorContext&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;EAutomationTestFlags&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ProductFilter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;FDamageCalculatorCasesTest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GetTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TArray&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FString&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TArray&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FString&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Commands&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Standard"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;Commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"100,30,70"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="n"&gt;Names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MinimumDamage"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;Commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"10,100,1"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;FDamageCalculatorCasesTest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RunTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FString&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TArray&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FString&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseIntoArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;","&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;TestEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Parameter count"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Num&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;BaseDamage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;Defense&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;Expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;LexTryParseString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseDamage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;LexTryParseString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Defense&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;LexTryParseString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;AddError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FString&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Could not parse numeric parameters: %s"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;TestEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Final damage"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;FDamageCalculator&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CalculateFinalDamage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseDamage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Defense&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;Expected&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FCString::Atoi&lt;/code&gt; turns invalid text into zero, which can hide broken test data. &lt;code&gt;LexTryParseString&lt;/code&gt; lets the test fail explicitly.&lt;/p&gt;

&lt;p&gt;For only a few scenarios, separate CQTest or Spec cases may be more readable. Complex Tests become especially useful when enumerating every map, Data Table row, or asset in a folder because each input appears as a separate test result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Work That Spans Multiple Frames
&lt;/h2&gt;

&lt;p&gt;Asset loading, level transitions, HTTP, async tasks, PIE startup, and similar work cannot finish in one frame. Do not block the Game Thread with &lt;code&gt;Sleep&lt;/code&gt;; wait for an observable completion condition.&lt;/p&gt;

&lt;p&gt;CQTest provides &lt;code&gt;TestCommandBuilder&lt;/code&gt;. The following assumes that the same &lt;code&gt;TEST_CLASS&lt;/code&gt; owns a &lt;code&gt;TUniquePtr&amp;lt;FProfileService&amp;gt;&lt;/code&gt; initialized in &lt;code&gt;BEFORE_EACH&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;TEST_METHOD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LoadProfile_CompletesWithSavedName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TestCommandBuilder&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;]()&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ProfileService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;BeginLoad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Player01"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Wait for profile loading to complete or fail"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;]()&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ProfileService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;IsLoadCompleted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
                    &lt;span class="n"&gt;ProfileService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;HasFailed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;FTimespan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;]()&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ASSERT_THAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IsFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProfileService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;HasFailed&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
            &lt;span class="n"&gt;ASSERT_THAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AreEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;FString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                &lt;span class="n"&gt;ProfileService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetLoadedProfile&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;PlayerName&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnTearDown&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;]()&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ProfileService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer &lt;code&gt;Until&lt;/code&gt; over a fixed &lt;code&gt;WaitDelay&lt;/code&gt;. A timeout must still exist: otherwise, a condition that never becomes true is one of the hardest CI failures to investigate. Include the operation, target ID, last observed state, and error code in diagnostics.&lt;/p&gt;

&lt;p&gt;Traditional Simple Automation Tests can use &lt;code&gt;DEFINE_LATENT_AUTOMATION_COMMAND&lt;/code&gt; and &lt;code&gt;ADD_LATENT_AUTOMATION_COMMAND&lt;/code&gt;. For new code, CQTest or Spec latent features often keep the sequence easier to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expected Warnings and Errors
&lt;/h2&gt;

&lt;p&gt;The Automation Framework records warnings and errors emitted during a test. Register an error first when the error is the expected behavior.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;AddExpectedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Inventory capacity exceeded"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;EAutomationExpectedErrorFlags&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The count is checked as well. Keep the matching text narrow: a broad &lt;code&gt;Contains&lt;/code&gt; pattern can accidentally absorb an unrelated error. Prefer a stable identifier and an exact occurrence count.&lt;/p&gt;

&lt;p&gt;Routine invalid input is often easier to test through return values or result types instead of Error logs. Reserve Error logging for genuinely abnormal states.&lt;/p&gt;

&lt;h2&gt;
  
  
  Functional Tests for Actors and Levels
&lt;/h2&gt;

&lt;p&gt;Use Functional Tests for behavior that depends on Actors, Components, Blueprints, or level wiring—for example, collecting a pickup, opening a door through a trigger, AI movement, or startup spawning.&lt;/p&gt;

&lt;p&gt;Enable the Functional Testing plugin and place a Functional Test Actor in a small dedicated test map. A C++ module that derives from &lt;code&gt;AFunctionalTest&lt;/code&gt; needs the &lt;code&gt;FunctionalTesting&lt;/code&gt; dependency.&lt;/p&gt;

&lt;p&gt;The lifecycle is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start preparation in &lt;code&gt;PrepareTest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Wait in &lt;code&gt;IsReady&lt;/code&gt; when preparation spans frames&lt;/li&gt;
&lt;li&gt;Perform assertions in &lt;code&gt;StartTest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;FinishTest&lt;/code&gt; for both success and failure&lt;/li&gt;
&lt;li&gt;Restore state during cleanup&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Forgetting &lt;code&gt;FinishTest&lt;/code&gt; leaves the test running until timeout.&lt;/p&gt;

&lt;p&gt;The following example expects an &lt;code&gt;APickupFunctionalTest&lt;/code&gt; instance in a dedicated map, with &lt;code&gt;PickupClass&lt;/code&gt; and &lt;code&gt;Inventory&lt;/code&gt; configured in Details. It is intentionally simplified. A production test should usually own a test Pawn or Fixture Actor and obtain the Component from that fixture instead of depending on ordinary gameplay state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// PickupFunctionalTest.h&lt;/span&gt;
&lt;span class="cp"&gt;#pragma once
&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"FunctionalTest.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"PickupFunctionalTest.generated.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;APickupActor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UInventoryComponent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;UCLASS&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MYGAMETESTS_API&lt;/span&gt; &lt;span class="n"&gt;APickupFunctionalTest&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;AFunctionalTest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GENERATED_BODY&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nl"&gt;protected:&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;PrepareTest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsReady_Implementation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;StartTest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;CleanUp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nl"&gt;private:&lt;/span&gt;
    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EditAnywhere&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;TSubclassOf&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;APickupActor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PickupClass&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EditInstanceOnly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;TObjectPtr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UInventoryComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;TWeakObjectPtr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;APickupActor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SpawnedPickup&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;bPreparationFinished&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;APickupFunctionalTest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;PrepareTest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Super&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;PrepareTest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;bPreparationFinished&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PickupClass&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Inventory&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;GetWorld&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;APickupActor&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Pickup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GetWorld&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SpawnActor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;APickupActor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;PickupClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;GetActorLocation&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;GetActorRotation&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Pickup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;SpawnedPickup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Pickup&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;RegisterAutoDestroyActor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Pickup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;bPreparationFinished&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;APickupFunctionalTest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;IsReady_Implementation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Wait only for preparation to finish so StartTest can report failures.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bPreparationFinished&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;APickupFunctionalTest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;StartTest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Super&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;StartTest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;PickupClass&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FinishTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EFunctionalTestResult&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PickupClass is not configured"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FinishTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EFunctionalTestResult&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Inventory is not configured"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;GetWorld&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FinishTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EFunctionalTestResult&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GetWorld() returned nullptr"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;SpawnedPickup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsValid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FinishTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EFunctionalTestResult&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to spawn APickupActor"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;Before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Inventory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetItemCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Potion"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;SpawnedPickup&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;After&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Inventory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetItemCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Potion"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;bSucceeded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AssertEqual_Int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;After&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Before&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Collecting the pickup adds one Potion"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;FinishTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;bSucceeded&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;EFunctionalTestResult&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Succeeded&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EFunctionalTestResult&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;bSucceeded&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Succeeded"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Item count did not increase"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;APickupFunctionalTest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CleanUp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;SpawnedPickup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;bPreparationFinished&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Super&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CleanUp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RegisterAutoDestroyActor&lt;/code&gt; removes the spawned Actor when the test ends. Do not wait forever in &lt;code&gt;IsReady&lt;/code&gt; when a class reference is missing or spawning failed. Complete preparation and report a specific failure in &lt;code&gt;StartTest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Prefer several small test maps over one giant functional-test map. Smaller maps reduce startup cost, hidden dependencies, and interference between tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Tests in the Editor
&lt;/h2&gt;

&lt;p&gt;In UE 5.8, open the automation interface from &lt;code&gt;Tools &amp;gt; Test Automation&lt;/code&gt;. Depending on the version, enabled plugins, and layout, it may also be available through &lt;code&gt;Tools &amp;gt; Session Frontend&lt;/code&gt; and its Automation tab.&lt;/p&gt;

&lt;p&gt;A typical workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable the required testing plugins&lt;/li&gt;
&lt;li&gt;Compile C++ and restart the Editor&lt;/li&gt;
&lt;li&gt;Open Test Automation&lt;/li&gt;
&lt;li&gt;Select tests under &lt;code&gt;MyGame&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Start the tests&lt;/li&gt;
&lt;li&gt;Inspect the result events for failures&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When a test is missing, verify the module is loaded, the context flags match, dependencies such as &lt;code&gt;CQTest&lt;/code&gt; are present, required plugins are enabled, and the Editor has been fully restarted. New registrations may not appear reliably after Live Coding alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Tests from the Command Line
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;Automation RunTest&lt;/code&gt; through &lt;code&gt;-ExecCmds&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="s2"&gt;"C:\Program Files\Epic Games\UE_5.8\Engine\Binaries\Win64\UnrealEditor-Cmd.exe"&lt;/span&gt; &lt;span class="se"&gt;^
&lt;/span&gt;  &lt;span class="s2"&gt;"D:\Projects\MyGame\MyGame.uproject"&lt;/span&gt; &lt;span class="se"&gt;^
&lt;/span&gt;  &lt;span class="na"&gt;-Unattended &lt;/span&gt;&lt;span class="se"&gt;^
&lt;/span&gt;  &lt;span class="na"&gt;-NoSplash &lt;/span&gt;&lt;span class="se"&gt;^
&lt;/span&gt;  &lt;span class="na"&gt;-NullRHI &lt;/span&gt;&lt;span class="se"&gt;^
&lt;/span&gt;  &lt;span class="na"&gt;-ExecCmds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Automation RunTest MyGame.Unit;Quit"&lt;/span&gt; &lt;span class="se"&gt;^
&lt;/span&gt;  &lt;span class="na"&gt;-ReportExportPath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"D:\Projects\MyGame\Saved\AutomationReports\Unit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-Unattended&lt;/code&gt; suppresses blocking dialogs. &lt;code&gt;-ReportExportPath&lt;/code&gt; writes report data, including JSON and HTML-related files. Preserve both the report directory and &lt;code&gt;Saved/Logs&lt;/code&gt; as CI artifacts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Not Trust Only the Process Exit Code
&lt;/h3&gt;

&lt;p&gt;Use the process result to detect startup failures, crashes, and job timeouts, but also parse the Automation Report JSON.&lt;/p&gt;

&lt;p&gt;Fail the CI job when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A test failed or timed out&lt;/li&gt;
&lt;li&gt;A required test unexpectedly remained &lt;code&gt;NotRun&lt;/code&gt; or incomplete&lt;/li&gt;
&lt;li&gt;The expected report JSON was not generated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Decide explicitly whether reasoned exclusions reported as &lt;code&gt;Skipped&lt;/code&gt; are allowed. Preserve reports even on success, and upload HTML-related output plus logs on failure.&lt;/p&gt;

&lt;p&gt;Functional Tests also need the map containing the test Actor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="s2"&gt;"C:\Program Files\Epic Games\UE_5.8\Engine\Binaries\Win64\UnrealEditor-Cmd.exe"&lt;/span&gt; &lt;span class="se"&gt;^
&lt;/span&gt;  &lt;span class="s2"&gt;"D:\Projects\MyGame\MyGame.uproject"&lt;/span&gt; &lt;span class="se"&gt;^
&lt;/span&gt;  &lt;span class="s2"&gt;"/Game/Tests/Maps/L_PickupFunctionalTest"&lt;/span&gt; &lt;span class="se"&gt;^
&lt;/span&gt;  &lt;span class="na"&gt;-Unattended &lt;/span&gt;&lt;span class="se"&gt;^
&lt;/span&gt;  &lt;span class="na"&gt;-NoSplash &lt;/span&gt;&lt;span class="se"&gt;^
&lt;/span&gt;  &lt;span class="na"&gt;-ExecCmds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Automation RunTest MyGame.Functional.Gameplay.Pickup;Quit"&lt;/span&gt; &lt;span class="se"&gt;^
&lt;/span&gt;  &lt;span class="na"&gt;-ReportExportPath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"D:\Projects\MyGame\Saved\AutomationReports\Pickup"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-NullRHI&lt;/code&gt; reduces cost when rendering is unnecessary. Remove it for screenshots, Slate, Viewports, Materials, GPU-specific checks, and tests marked &lt;code&gt;NonNullRHI&lt;/code&gt;. Separate rendering and non-rendering jobs instead of assuming every Functional Test can run headlessly.&lt;/p&gt;

&lt;p&gt;Groups can be defined in &lt;code&gt;DefaultEngine.ini&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[/Script/AutomationController.AutomationControllerSettings]&lt;/span&gt;
&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="py"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;(Name="PreSubmit", Filters=((Contains="MyGame.Unit.", MatchFromStart=true)))&lt;/span&gt;
&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="py"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;(Name="Nightly", Filters=((Contains="MyGame.Functional.", MatchFromStart=true)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="na"&gt;-ExecCmds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Automation RunTest Group:PreSubmit;Quit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Temporarily exclude a flaky test through configuration instead of commenting it out, and include both a reason and a removal condition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="py"&gt;ExcludeTest&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;(Test="MyGame.Functional.Gameplay.Pickup",Reason="UE-12345: Remove after fixing spawn wait",Warn=False)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A Practical CI Split
&lt;/h2&gt;

&lt;p&gt;Running every test on every change makes feedback too slow. Running everything only at night allows several unrelated changes to accumulate before a failure appears.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pull Request or Pre-Submit
&lt;/h3&gt;

&lt;p&gt;Keep this within a few minutes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CQTest and Simple Automation Tests for deterministic C++&lt;/li&gt;
&lt;li&gt;Important serialization round trips&lt;/li&gt;
&lt;li&gt;Critical data validation&lt;/li&gt;
&lt;li&gt;Smoke Tests that finish in roughly one second&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;code&gt;-NullRHI&lt;/code&gt; when rendering is not required.&lt;/p&gt;

&lt;h3&gt;
  
  
  After Updating the Main Branch
&lt;/h3&gt;

&lt;p&gt;Add moderately expensive integration checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small Functional Tests&lt;/li&gt;
&lt;li&gt;Important map loads&lt;/li&gt;
&lt;li&gt;Blueprint compilation&lt;/li&gt;
&lt;li&gt;Save/load with real files&lt;/li&gt;
&lt;li&gt;Asset Registry reference validation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Nightly or Scheduled Jobs
&lt;/h3&gt;

&lt;p&gt;Reserve expensive environments for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Loading all maps and major assets&lt;/li&gt;
&lt;li&gt;Screenshot comparisons&lt;/li&gt;
&lt;li&gt;Multiple clients and a Dedicated Server&lt;/li&gt;
&lt;li&gt;Packaged builds&lt;/li&gt;
&lt;li&gt;Long-running memory or stress tests&lt;/li&gt;
&lt;li&gt;Target-device execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where Gauntlet becomes useful. It can launch and coordinate multiple Unreal sessions, clients, and servers. Do not move ordinary rule tests into Gauntlet; use it when the packaged runtime or process topology is part of what must be proven.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python Automation Tests for Content Workflows
&lt;/h2&gt;

&lt;p&gt;Python tests are useful for naming rules, folder structure, import settings, and forbidden references. With the Python Automation Test support enabled, scripts under &lt;code&gt;Content/Python&lt;/code&gt; named &lt;code&gt;test_*.py&lt;/code&gt; can be discovered.&lt;/p&gt;

&lt;p&gt;The following sample uses &lt;code&gt;EditorAssetLibrary&lt;/code&gt;, so it assumes an Editor environment with the required Python and Editor Scripting plugins. Confirm API and plugin names for your exact engine version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unreal&lt;/span&gt;

&lt;span class="n"&gt;assets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unreal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EditorAssetLibrary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_assets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/Game/Characters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;recursive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;include_folder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;invalid_assets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;assets&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SK_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SM_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;T_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MI_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;max_reported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="n"&gt;preview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;invalid_assets&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;max_reported&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;omitted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invalid_assets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;max_reported&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;omitted_note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;...and &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;omitted&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; more&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;omitted&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;invalid_assets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Naming violations: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invalid_assets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;preview&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;omitted_note&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rename assets to use SK_, SM_, T_, or MI_.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not dump thousands of paths into CI logs. Report the count, a limited preview, and the correction rule. When combining several checks, log each summary and raise an exception at the end; logging errors without failing can leave the CI job green.&lt;/p&gt;

&lt;p&gt;Python execution does not automatically advance Editor ticks during asynchronous work. Split such operations with &lt;code&gt;unreal.AutomationScheduler.add_latent_command&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automation Driver for UI Input
&lt;/h2&gt;

&lt;p&gt;Automation Driver simulates mouse input, clicks, keys, scrolling, and drag-and-drop. Give UI elements stable driver IDs instead of locating them only by visible text or hierarchy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;SNew&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STextBlock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;FViewModel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GetPlayerName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddMetaData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FDriverMetaData&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PlayerNameLabel"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The driver is disabled by default. Enable it at test start and always disable it during cleanup.&lt;/p&gt;

&lt;p&gt;Synchronous Driver APIs block until completion. Calling them on the Game Thread can deadlock. Keep input operations outside the Game Thread, but do not move Slate or UObject validation indiscriminately to a worker thread. Slate &lt;code&gt;TSharedPtr&lt;/code&gt; instances are not thread-safe by default and should not be copied into worker-thread lambdas.&lt;/p&gt;

&lt;p&gt;A safe conceptual split is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BeforeEach [Game Thread]
  Create the Widget, Window, ViewModel, and Driver; enable the Driver
        ↓
Input phase [outside Game Thread]
  Perform clicks, typing, scrolling, and other synchronous Driver actions
        ↓
Validation phase [Game Thread]
  Inspect UObject, Slate, and ViewModel state
        ↓
AfterEach [Game Thread]
  Destroy the Driver, close the Window, and disable the Driver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use Spec async blocks or latent commands to return to the Game Thread after input completes. Ensure failure paths still reach &lt;code&gt;Disable()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;UI tests are slower and more fragile than service-layer tests. Limit them to representative flows such as starting from the title screen, saving settings, or selecting a save slot. Test detailed branches in C++ beneath the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Low-Level Tests
&lt;/h2&gt;

&lt;p&gt;UE5 Low-Level Tests use Catch2 and run through a dedicated test executable with their own &lt;code&gt;.Build.cs&lt;/code&gt; and &lt;code&gt;.Target.cs&lt;/code&gt;. Avoiding full Editor startup can make a large low-level suite much faster.&lt;/p&gt;

&lt;p&gt;They are not limited strictly to pure C++; with the right target and modules, they can exercise UObject, assets, and Engine Components. However, they require dedicated build and CI setup, and World- or Editor-workflow-heavy tests may remain easier in Automation or Functional Tests.&lt;/p&gt;

&lt;p&gt;Consider Low-Level Tests when you already have hundreds of small tests, Editor startup dominates execution time, or BuildGraph should run them as an independent job. For the first test in a project, CQTest or a Simple Automation Test is usually the shorter path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Failure Modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Waiting with Sleep
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;FPlatformProcess::Sleep&lt;/code&gt; can stop the Game Thread from ticking. Use latent actions and observable completion conditions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Depending on Test Order
&lt;/h3&gt;

&lt;p&gt;Each test must create its own state and restore it during teardown. A test that assumes a previous test already logged in will fail in isolation or parallel execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leaving Files or Actors Behind
&lt;/h3&gt;

&lt;p&gt;Register generated Actors with &lt;code&gt;RegisterAutoDestroyActor&lt;/code&gt;. Write files under a test-only temporary directory and remove them in teardown. Clean stale artifacts before startup as well, and make cleanup run after failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparing Floating-Point Values Exactly
&lt;/h3&gt;

&lt;p&gt;Use an explicit tolerance for physics, timing, and coordinates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;TestTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X is within tolerance"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;FMath&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;IsNearlyEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Actual&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating Unreproducible Random Tests
&lt;/h3&gt;

&lt;p&gt;Fix the seed and print both the seed and generated input on failure. Randomized testing should supplement deterministic boundary cases, not replace them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing Vague Failure Messages
&lt;/h3&gt;

&lt;p&gt;Include the condition, expected value, actual value, and target ID. A developer should be able to begin investigation from the CI log alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expecting Live Coding to Register Everything
&lt;/h3&gt;

&lt;p&gt;When a new test is missing from the list, close the Editor, run a normal build, and restart it. Registration depends on module loading.&lt;/p&gt;

&lt;h3&gt;
  
  
  Starting with Coverage Percentage
&lt;/h3&gt;

&lt;p&gt;Begin with failures that matter: save compatibility, currency rules, combat boundaries, data references, or a level that cannot start. Frequently changed rules and QA flows that are checked manually every time are also strong candidates.&lt;/p&gt;

&lt;p&gt;The first objective is not a percentage. It is preventing one important regression from returning.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Gradual Adoption Plan
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Add one World-independent rule test with CQTest or a Simple Automation Test.&lt;/li&gt;
&lt;li&gt;Add a regression test before fixing the next reproducible bug.&lt;/li&gt;
&lt;li&gt;Automate save/load compatibility and static-data validation.&lt;/li&gt;
&lt;li&gt;Stabilize one representative Functional Test in a small dedicated map.&lt;/li&gt;
&lt;li&gt;Commit the command-line scripts so local and CI execution use the same commands.&lt;/li&gt;
&lt;li&gt;Track duration, retry success rate, and timeout locations, then remove flakiness instead of normalizing reruns.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Unreal Engine testing works best as a layered system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CQTest, Automation Spec, and Simple or Complex Automation Tests for fast C++ behavior&lt;/li&gt;
&lt;li&gt;Functional Tests for Actors, Components, Blueprints, and level wiring&lt;/li&gt;
&lt;li&gt;Python Automation Tests for content-production rules&lt;/li&gt;
&lt;li&gt;Automation Driver for a small number of representative UI flows&lt;/li&gt;
&lt;li&gt;Gauntlet for packaged builds and multi-process environments&lt;/li&gt;
&lt;li&gt;Low-Level Tests when dedicated execution speed justifies the extra build setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For multi-frame work, wait for explicit completion or failure conditions with a clear timeout. In CI, parse Automation Reports instead of trusting only the Editor process exit code.&lt;/p&gt;

&lt;p&gt;Do not start by building an end-to-end test for the whole game. Start with deterministic tests that run quickly and fail with enough information to diagnose the problem.&lt;/p&gt;

&lt;p&gt;The useful milestone is not “we reached a coverage target.” It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When an important old bug returns, the build stops automatically.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.unrealengine.com/news/unreal-engine-5-8-is-now-available" rel="noopener noreferrer"&gt;Unreal Engine 5.8 is now available&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/automation-test-framework-in-unreal-engine" rel="noopener noreferrer"&gt;Automation Test Framework in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/write-cplusplus-tests-in-unreal-engine" rel="noopener noreferrer"&gt;Write C++ Tests in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/cqtest-test-framework-for-unreal-engine" rel="noopener noreferrer"&gt;CQTest Test Framework for Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/automation-spec-in-unreal-engine" rel="noopener noreferrer"&gt;Automation Spec in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/functional-testing-in-unreal-engine" rel="noopener noreferrer"&gt;Functional Testing in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/run-automation-tests-in-unreal-engine" rel="noopener noreferrer"&gt;Run Automation Tests in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/configure-automation-tests-in-unreal-engine" rel="noopener noreferrer"&gt;Configure Automation Tests in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/automation-driver-in-unreal-engine" rel="noopener noreferrer"&gt;Automation Driver in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/write-editor-tests-with-python-in-unreal-engine" rel="noopener noreferrer"&gt;Write Editor Tests with Python in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/gauntlet-automation-framework-in-unreal-engine" rel="noopener noreferrer"&gt;Gauntlet Automation Framework in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/low-level-tests-in-unreal-engine" rel="noopener noreferrer"&gt;Low-Level Tests in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unrealengine</category>
      <category>cpp</category>
      <category>testing</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>A Practical Guide to Testing in Unity: EditMode, PlayMode, Async, and CI</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Thu, 30 Jul 2026 13:03:01 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/a-practical-guide-to-testing-in-unity-editmode-playmode-async-and-ci-2b8e</link>
      <guid>https://dev.to/gamedevtoollab/a-practical-guide-to-testing-in-unity-editmode-playmode-async-and-ci-2b8e</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Testing in Unity is more complicated than testing ordinary C# code.&lt;/p&gt;

&lt;p&gt;A damage formula can be covered with a normal NUnit &lt;code&gt;[Test]&lt;/code&gt;, but a real game also contains &lt;code&gt;MonoBehaviour&lt;/code&gt;, &lt;code&gt;ScriptableObject&lt;/code&gt;, frame updates, coroutines, scenes, prefabs, physics, and asynchronous asset loading.&lt;/p&gt;

&lt;p&gt;The practical questions are where to split EditMode and PlayMode, when &lt;code&gt;[UnityTest]&lt;/code&gt; is necessary, how to handle lifecycle and asynchronous code, and which tests belong in CI.&lt;/p&gt;

&lt;p&gt;This article uses Unity 6 and Unity Test Framework 1.6 as its baseline. It is written for programmers introducing automated tests or adding them incrementally to an existing project. The samples are intentionally small, so adjust omitted namespaces and project-specific types to your own assemblies.&lt;/p&gt;

&lt;p&gt;The basic strategy is to keep game rules in fast pure-C# EditMode tests, test thin Unity adapters only where needed, reserve PlayMode for PlayerLoop or lifecycle behavior, and leave visual quality or device-specific performance to QA and device tests.&lt;/p&gt;

&lt;p&gt;Tests do not prove that a game has no bugs. They provide a fast safety net when you refactor code, change specifications, or fix a regression.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Unity Test Framework provides
&lt;/h2&gt;

&lt;p&gt;Unity Test Framework is Unity's built-in testing system. It is based on NUnit and adds support for Unity-specific concepts such as frames, the application loop, domain reloads, and player execution.&lt;/p&gt;

&lt;p&gt;The common ways to run tests are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test Runner inside the Unity Editor&lt;/li&gt;
&lt;li&gt;Unity command-line execution for CI&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TestRunnerApi&lt;/code&gt; for custom tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use Test Runner during development, command-line execution in CI, and &lt;code&gt;TestRunnerApi&lt;/code&gt; only when you need a custom test workflow.&lt;/p&gt;

&lt;p&gt;Unity ships its own NUnit-compatible environment, so APIs from the latest standalone NUnit may not all be available or behave identically. Check the documentation for the Test Framework version bundled with your Editor, especially for asynchronous assertions.&lt;/p&gt;




&lt;h2&gt;
  
  
  EditMode and PlayMode tests
&lt;/h2&gt;

&lt;p&gt;Unity tests are broadly divided into EditMode and PlayMode tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  EditMode tests
&lt;/h3&gt;

&lt;p&gt;EditMode tests run in the Editor without normal Play Mode. They suit calculations, save migration, probability rules, data validation, &lt;code&gt;ScriptableObject&lt;/code&gt; logic, editor tooling, serialization, and static prefab or scene checks. Pure C# EditMode tests are usually fast enough to run continuously.&lt;/p&gt;

&lt;h3&gt;
  
  
  PlayMode tests
&lt;/h3&gt;

&lt;p&gt;PlayMode tests execute in Unity's runtime environment. Use them for lifecycle callbacks, frame updates, coroutines, scene loading, runtime prefab wiring, animation transitions, physics, and behavior that appears only in a Player. They are slower and more sensitive to global state, so an all-PlayMode suite becomes expensive to diagnose.&lt;/p&gt;

&lt;h3&gt;
  
  
  A practical decision process
&lt;/h3&gt;

&lt;p&gt;Ask these questions in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the behavior work without Unity APIs?&lt;/li&gt;
&lt;li&gt;Does it need Unity objects but not frame progression?&lt;/li&gt;
&lt;li&gt;Does it depend on the PlayerLoop, lifecycle events, scenes, or physics?&lt;/li&gt;
&lt;li&gt;Does it require an actual target device?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use pure EditMode tests for case 1, Unity-aware EditMode tests for case 2, PlayMode tests for case 3, and Player/device testing for case 4.&lt;/p&gt;




&lt;h2&gt;
  
  
  Create test assemblies first
&lt;/h2&gt;

&lt;p&gt;Tests should live in assemblies that reference NUnit and the production assemblies they test.&lt;/p&gt;

&lt;p&gt;A practical folder structure is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Assets/
├── Game/
│   ├── Runtime/
│   │   ├── Game.Runtime.asmdef
│   │   ├── Battle/
│   │   └── Save/
│   └── Editor/
│       ├── Game.Editor.asmdef
│       └── Validation/
└── Tests/
    ├── EditMode/
    │   ├── Game.EditModeTests.asmdef
    │   ├── Battle/
    │   └── Save/
    └── PlayMode/
        ├── Game.PlayModeTests.asmdef
        ├── Battle/
        └── Scene/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An EditMode test assembly conceptually looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Game.EditModeTests"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"references"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Game.Runtime"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includePlatforms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Editor"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"optionalUnityReferences"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"TestAssemblies"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A PlayMode test assembly can target runtime platforms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Game.PlayModeTests"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"references"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Game.Runtime"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includePlatforms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"optionalUnityReferences"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"TestAssemblies"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treat these JSON blocks as conceptual examples. Unity versions can generate different fields, so verify the settings in the Inspector instead of blindly replacing an &lt;code&gt;.asmdef&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;A common obstacle in older projects is that production code still belongs to &lt;code&gt;Assembly-CSharp.dll&lt;/code&gt;, which a test assembly cannot directly reference. Move one independent class into &lt;code&gt;Game.Runtime.asmdef&lt;/code&gt;, reference it from the test assembly, and expand gradually. Keep &lt;code&gt;UnityEditor&lt;/code&gt; APIs in a separate editor assembly and preserve a one-way dependency from presentation code toward lower-level logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Write the first pure C# test
&lt;/h2&gt;

&lt;p&gt;Start with a class that has no Unity dependency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Game.Battle&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DamageCalculator&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;Calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;defense&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attack&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;defense&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test is ordinary NUnit code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Game.Battle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NUnit.Framework&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Game.Tests.EditMode.Battle&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DamageCalculatorTests&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Calculate_AttackIsGreaterThanDefense_ReturnsDifference&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Arrange&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;calculator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DamageCalculator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// Act&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;calculator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;defense&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Assert&lt;/span&gt;
            &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;70&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Calculate_DefenseIsGreaterThanAttack_ReturnsMinimumDamage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;calculator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DamageCalculator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;calculator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;defense&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arrange, Act, and Assert are useful as a mental model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Arrange the subject and inputs.&lt;/li&gt;
&lt;li&gt;Act by executing one behavior.&lt;/li&gt;
&lt;li&gt;Assert the result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Comments are not mandatory in every short test. Clear spacing is often enough.&lt;/p&gt;

&lt;p&gt;Test names should explain a failure. A useful convention is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Method_Condition_ExpectedResult
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Calculate_DefenseIsGreaterThanAttack_ReturnsMinimumDamage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact convention matters less than being able to infer what broke from the failed test name in CI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parameterize boundary cases
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;[TestCase]&lt;/code&gt; when the same rule must be checked with multiple inputs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;70&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Calculate_WithVariousParameters_ReturnsExpectedDamage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;defense&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;calculator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DamageCalculator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;calculator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;defense&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parameterization is especially useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;minimum and maximum values&lt;/li&gt;
&lt;li&gt;zero, one, and negative values&lt;/li&gt;
&lt;li&gt;just below, exactly at, and just above a threshold&lt;/li&gt;
&lt;li&gt;level and inventory caps&lt;/li&gt;
&lt;li&gt;date boundaries&lt;/li&gt;
&lt;li&gt;probability-table edges&lt;/li&gt;
&lt;li&gt;rounding rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For more complex cases, use &lt;code&gt;[TestCaseSource]&lt;/code&gt;. &lt;code&gt;TestCaseData&lt;/code&gt; allows readable names in CI output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;TestCaseData&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;Cases&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TestCaseData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;70&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AttackGreaterThanDefense"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TestCaseData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DefenseGreaterThanAttack"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;TestCaseSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Cases&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Calculate_WithCases_ReturnsExpected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;defense&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;calculator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DamageCalculator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calculator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;defense&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not combine unrelated rules merely because the inputs fit into one parameter list. Parameterize data variations of the same behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  Assert behavior, not every field
&lt;/h2&gt;

&lt;p&gt;A test becomes difficult to understand when it verifies many unrelated values. The goal is not "one assertion per test" but "one behavior per test."&lt;/p&gt;

&lt;p&gt;Multiple assertions are appropriate when they describe one atomic result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ConsumePotion_WhenStockExists_HealsAndDecreasesStock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;potionCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentHp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxHp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConsumePotion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentHp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;80&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PotionCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Healing the player and consuming one item are two parts of the same operation. Splitting them would duplicate setup without improving clarity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test ScriptableObject rules in EditMode
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;ScriptableObject&lt;/code&gt; can be created without saving an asset.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NUnit.Framework&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EnemyParameterTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;EnemyParameter&lt;/span&gt; &lt;span class="n"&gt;_parameter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SetUp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetUp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_parameter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ScriptableObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateInstance&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EnemyParameter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TearDown&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;TearDown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DestroyImmediate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_parameter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;IsBoss_HpIsAtLeast10000_ReturnsTrue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_parameter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_parameter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsBoss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;True&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The production type might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CreateAssetMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;menuName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Game/Enemy Parameter"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EnemyParameter&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ScriptableObject&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsBoss&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Destroy temporary EditMode objects with &lt;code&gt;DestroyImmediate&lt;/code&gt; when appropriate. Put cleanup in &lt;code&gt;[TearDown]&lt;/code&gt; so it still runs after an assertion failure.&lt;/p&gt;

&lt;p&gt;Separate two concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test class rules with temporary instances.&lt;/li&gt;
&lt;li&gt;Validate all real project assets in a dedicated project-validation test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tests that depend on exact asset paths or GUIDs are more fragile than tests against temporary instances.&lt;/p&gt;




&lt;h2&gt;
  
  
  Validate prefabs and assets
&lt;/h2&gt;

&lt;p&gt;Automated tests are also effective at finding authoring mistakes.&lt;/p&gt;

&lt;p&gt;Suppose every enemy prefab under &lt;code&gt;Assets/Game/Enemies&lt;/code&gt; must have an &lt;code&gt;EnemyController&lt;/code&gt; on its root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Linq&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NUnit.Framework&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEditor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EnemyPrefabValidationTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;EnemyPrefabs_AllHaveEnemyController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;guids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AssetDatabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAssets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"t:Prefab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Assets/Game/Enemies"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;invalidPaths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;guids&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AssetDatabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GUIDToAssetPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;prefab&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AssetDatabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadAssetAtPath&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prefab&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
                       &lt;span class="n"&gt;prefab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EnemyController&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;invalidPaths&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;$"Prefabs without EnemyController:\n&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\n"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;invalidPaths&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is closer to project validation than a unit test, but it is highly valuable. The same approach can detect missing components or scripts, broken references, duplicate IDs, invalid Addressables labels, missing localization keys, incorrect Layers or Tags, and assets that violate project rules.&lt;/p&gt;

&lt;p&gt;The sample above only checks for &lt;code&gt;EnemyController&lt;/code&gt;. Missing scripts require a separate validation, for example with &lt;code&gt;GameObjectUtility.GetMonoBehavioursWithMissingScriptCount&lt;/code&gt; while traversing the prefab hierarchy.&lt;/p&gt;

&lt;p&gt;A full project scan may be slow. Categorize it separately and run it in CI or at night instead of blocking every local edit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AssetValidation"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;EnemyPrefabs_AllHaveEnemyController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Validation logic&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Test MonoBehaviour correctly
&lt;/h2&gt;

&lt;p&gt;Never instantiate a &lt;code&gt;MonoBehaviour&lt;/code&gt; with &lt;code&gt;new&lt;/code&gt;. Create a &lt;code&gt;GameObject&lt;/code&gt; and use &lt;code&gt;AddComponent&amp;lt;T&amp;gt;()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A method that does not require frame progression can still be tested in EditMode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NUnit.Framework&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HealthViewTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;_gameObject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SetUp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetUp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_gameObject&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HealthViewTest"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TearDown&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;TearDown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DestroyImmediate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_gameObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetHealth_WithHalfValue_StoresRatio&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HealthView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

        &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHealth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ratio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.5f&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Within&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.0001f&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not test &lt;code&gt;Awake&lt;/code&gt; or &lt;code&gt;Start&lt;/code&gt;. It only verifies a public method on a component. Move to PlayMode when Unity lifecycle behavior is part of the specification.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cross frames with UnityTest
&lt;/h2&gt;

&lt;p&gt;When a PlayMode test must cross frames, use &lt;code&gt;[UnityTest]&lt;/code&gt; and return &lt;code&gt;IEnumerator&lt;/code&gt;. The attribute is also available in EditMode, but this example focuses on runtime lifecycle behavior.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerInitializer&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsInitialized&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;IsInitialized&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PlayMode test waits one frame before checking the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NUnit.Framework&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine.TestTools&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerInitializerTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UnityTest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;Start_AfterOneFrame_InitializesPlayer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;gameObject&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Player"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PlayerInitializer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsInitialized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;True&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;yield return null&lt;/code&gt; waits until the next frame.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Object.Destroy&lt;/code&gt; is delayed until the end of the frame. &lt;code&gt;try/finally&lt;/code&gt; ensures that destruction is requested even when an assertion fails. For fixtures that create multiple objects, or when destruction must complete before the next test, centralize cleanup in &lt;code&gt;[UnityTearDown]&lt;/code&gt; and wait one frame.&lt;/p&gt;

&lt;p&gt;Avoid arbitrary frame waits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Avoid this when there is no reason for exactly ten frames.&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer a completion condition with an upper bound:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UnityTest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;Load_WhenStarted_CompletesWithinFrames&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;gameObject&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Loader"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TestLoader&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Begin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxFrames&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;maxFrames&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCompleted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCompleted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"Loading did not complete within 120 frames."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not use a frame limit for work whose duration depends heavily on machine performance or real I/O. For those cases, inject the dependency or expose an explicit completion signal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choose Test or UnityTest by behavior
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;[Test]&lt;/code&gt; when the operation is synchronous and does not need frame progression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Calculate_WhenCalled_ReturnsExpected&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Synchronous behavior&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;[UnityTest]&lt;/code&gt; when you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;yield return null&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;coroutine progression&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WaitForFixedUpdate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;lifecycle changes across frames&lt;/li&gt;
&lt;li&gt;Unity-specific yield instructions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A test in a PlayMode assembly does not automatically require &lt;code&gt;[UnityTest]&lt;/code&gt;. A normal &lt;code&gt;[Test]&lt;/code&gt; is valid there when it completes synchronously.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keep physics tests broad and deterministic
&lt;/h2&gt;

&lt;p&gt;A physics test may wait for fixed updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UnityTest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;Rigidbody_AfterFixedUpdate_FallsDown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;gameObject&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreatePrimitive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PrimitiveType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cube&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Rigidbody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;initialY&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WaitForFixedUpdate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WaitForFixedUpdate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LessThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initialY&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This verifies only the broad behavior: gravity moves the object downward. Exact coordinates after a fixed number of frames are fragile because they depend on Unity version, physics settings, fixed delta time, and floating-point behavior.&lt;/p&gt;

&lt;p&gt;Prefer these strategies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use tolerances instead of exact floating-point equality&lt;/li&gt;
&lt;li&gt;verify direction or range rather than a precise final coordinate&lt;/li&gt;
&lt;li&gt;extract your own decision logic into pure C# tests&lt;/li&gt;
&lt;li&gt;set and restore physics settings explicitly&lt;/li&gt;
&lt;li&gt;isolate high-precision simulation in a dedicated suite&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need to retest Unity's physics engine. Test that your code configures it correctly and interprets its results correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test asynchronous code without real waiting
&lt;/h2&gt;

&lt;p&gt;Unity Test Framework supports tests that return &lt;code&gt;.NET Task&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Threading.Tasks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NUnit.Framework&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRepositoryTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync_ExistingUser_ReturnsUser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FakeUserRepository&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid real delays:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Avoid this in an ordinary unit test.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twenty five-second tests already add 100 seconds. Real network access also makes results depend on authentication, server data, and connectivity.&lt;/p&gt;

&lt;p&gt;Most asynchronous tests should verify the result, error conversion, cancellation, state update, and duplicate-execution rules rather than elapsed time.&lt;/p&gt;

&lt;p&gt;Inject external work behind an interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IUserApi&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IUserApi&lt;/span&gt; &lt;span class="n"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IUserApi&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_api&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a fake that completes immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FakeUserApi&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IUserApi&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;UserDto&lt;/span&gt; &lt;span class="n"&gt;_response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;FakeUserApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserDto&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then test conversion without network access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync_ApiSucceeds_ConvertsDto&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FakeUserApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserDto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep real connectivity checks in a small integration suite with controlled credentials and environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Awaitable and UniTask require extra care
&lt;/h2&gt;

&lt;p&gt;Unity 6 projects increasingly use &lt;code&gt;Awaitable&lt;/code&gt;, while many existing projects use UniTask.&lt;/p&gt;

&lt;p&gt;Separate two questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What return type does the test method use?&lt;/li&gt;
&lt;li&gt;What asynchronous type does production code return?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Unity Test Framework 1.6, prefer supported &lt;code&gt;async Task&lt;/code&gt; test methods. Use &lt;code&gt;[UnityTest]&lt;/code&gt; and &lt;code&gt;IEnumerator&lt;/code&gt; for coroutine-style frame tests. Production code may return &lt;code&gt;Awaitable&lt;/code&gt; or UniTask, but tests must be able to observe completion.&lt;/p&gt;

&lt;p&gt;Being awaitable does not mean a workflow is safe in an EditMode test. Pure C# transformations and cancellation rules belong in EditMode. Operations based on &lt;code&gt;Awaitable.NextFrameAsync&lt;/code&gt;, UniTask &lt;code&gt;Yield&lt;/code&gt; or &lt;code&gt;DelayFrame&lt;/code&gt;, scene loading, Addressables, and Unity object lifecycles should be tested in PlayMode or in a built Player.&lt;/p&gt;

&lt;p&gt;Important constraints are that Unity &lt;code&gt;Awaitable&lt;/code&gt; instances are pooled and must not be awaited twice; UniTask should also be treated as single-consumption unless its documented preservation mechanisms are used. PlayerLoop-dependent APIs can differ across EditMode, PlayMode, Player, and BatchMode. Return to the main thread before touching Unity APIs, avoid &lt;code&gt;async void&lt;/code&gt;, &lt;code&gt;UniTaskVoid&lt;/code&gt;, and &lt;code&gt;.Forget()&lt;/code&gt; in testable inner APIs, and provide cancellation for operations that may not complete.&lt;/p&gt;

&lt;p&gt;A production event handler may need fire-and-forget behavior. Keep the inner operation awaitable and test that inner method directly. Otherwise an exception or state change can occur after the test has already finished and leak into another test.&lt;/p&gt;

&lt;p&gt;Also run PlayerLoop-dependent asynchronous tests in CI with &lt;code&gt;-batchmode&lt;/code&gt;. Do not terminate the Editor before the test result XML and exit code are produced.&lt;/p&gt;




&lt;h2&gt;
  
  
  Inject time and randomness
&lt;/h2&gt;

&lt;p&gt;Current time and randomness are common sources of flaky tests.&lt;/p&gt;

&lt;p&gt;Avoid reading local time inside business logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;CanReceiveDailyBonus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Date&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_lastReceivedAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result changes at a date boundary and may differ across CI time zones.&lt;/p&gt;

&lt;p&gt;Make the clock explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IClock&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;UtcNow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DailyBonusService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IClock&lt;/span&gt; &lt;span class="n"&gt;_clock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;DailyBonusService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IClock&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_clock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;CanReceive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;lastReceivedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Date&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
               &lt;span class="n"&gt;lastReceivedAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUniversalTime&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a fixed clock in tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FixedClock&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IClock&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;FixedClock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;UtcNow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUniversalTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;UtcNow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CanReceive_WhenUtcDateChanged_ReturnsTrue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FixedClock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DailyBonusService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CanReceive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;59&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;59&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;True&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual product specification must also define whether the source of truth is UTC, server time, or a region-specific reset time.&lt;/p&gt;

&lt;p&gt;Randomness should be injected in the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IRandom&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;minInclusive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxExclusive&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fake can return a chosen value so rare drops and critical-hit branches are deterministic. This also helps replay systems, debugging, and synchronization with a server.&lt;/p&gt;




&lt;h2&gt;
  
  
  A mock library is optional
&lt;/h2&gt;

&lt;p&gt;Do not introduce a mocking framework merely because an article about testing mentions mocks. A handwritten fake is often clearer in Unity projects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RecordingAnalytics&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAnalytics&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Events&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Purchase_WhenSucceeded_SendsAnalyticsEvent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RecordingAnalytics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PurchaseService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Does&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"purchase_completed"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handwritten fakes are easy to inspect, debug, and keep compatible across Unity and .NET versions. A mocking library becomes useful when many dependencies require extensive call and argument verification.&lt;/p&gt;

&lt;p&gt;If mock setup dominates the test, the production class may have too many responsibilities.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test exceptions and logs
&lt;/h2&gt;

&lt;p&gt;For synchronous exceptions, use &lt;code&gt;Assert.Throws&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Constructor_MaxHpIsZero_ThrowsArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Throws&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxHp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be careful with &lt;code&gt;Assert.ThrowsAsync&lt;/code&gt; in Unity. Unity's documentation warns that blocking the caller while an operation needs the main thread can freeze the Editor. A safe pattern is to await in an &lt;code&gt;async Task&lt;/code&gt; test and inspect the exception with &lt;code&gt;try/catch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync_WhenApiFails_ThrowsUserLoadException&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UserLoadException was not thrown."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserLoadException&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrorCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"network_error"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For failures that are part of normal control flow, a result type can be clearer than exceptions.&lt;/p&gt;

&lt;p&gt;Unity normally fails a test when an unexpected error or exception is logged. Register expected logs explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NUnit.Framework&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine.TestTools&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Load_InvalidId_LogsError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;LogAssert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Invalid user id: -1"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserLoader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a regular expression for dynamic values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text.RegularExpressions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;LogAssert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;LogType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;@"Invalid user id: -?\d+"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not use logs as the only failure signal when callers must react. Return a result, throw a meaningful exception, or expose a state that can be asserted.&lt;/p&gt;




&lt;h2&gt;
  
  
  Isolate state with SetUp and TearDown
&lt;/h2&gt;

&lt;p&gt;Every test must behave the same alone and as part of the full suite.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerControllerTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;_gameObject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;PlayerController&lt;/span&gt; &lt;span class="n"&gt;_controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SetUp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetUp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_gameObject&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Player"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_controller&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PlayerController&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TearDown&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;TearDown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DestroyImmediate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_gameObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetSpeed_PositiveValue_UpdatesSpeed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetSpeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Speed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5f&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In PlayMode, destruction is delayed. Use &lt;code&gt;[UnityTearDown]&lt;/code&gt; when you must destroy multiple objects and wait for completion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_createdObjects&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UnityTearDown&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;TearDown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;gameObject&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_createdObjects&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gameObject&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;_createdObjects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Record each created object immediately after creation so cleanup still occurs after an assertion failure.&lt;/p&gt;

&lt;p&gt;Common leaks include static fields, singletons, &lt;code&gt;PlayerPrefs&lt;/code&gt;, time and physics settings, &lt;code&gt;Random.state&lt;/code&gt;, scenes, temporary assets, event subscriptions, cancellation sources, Addressables handles, and &lt;code&gt;DontDestroyOnLoad&lt;/code&gt; objects.&lt;/p&gt;

&lt;p&gt;Save global settings before changing them and restore them in teardown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;_originalTimeScale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SetUp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetUp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_originalTimeScale&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeScale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeScale&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TearDown&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;TearDown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeScale&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_originalTimeScale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Never depend on test order
&lt;/h2&gt;

&lt;p&gt;A suite that assumes this order is fragile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. CreateUser
2. UpdateUser
3. DeleteUser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UpdateUser&lt;/code&gt; fails when run by itself. Each test should create and remove its own state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;UpdateUser_ExistingUser_ChangesName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InMemoryUserRepository&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Before"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"After"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"After"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Long end-to-end flows can be useful, but treat them as a separate scenario layer rather than chaining unit tests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Write tests that survive refactoring
&lt;/h2&gt;

&lt;p&gt;Test observable behavior instead of implementation details.&lt;/p&gt;

&lt;p&gt;Private methods are implementation details; extract a separate responsibility when it deserves direct testing. Call counts alone do not prove player-visible behavior, so combine interactions with output or state assertions. Avoid hard-coding Transform hierarchy details: a UI test tied to a path such as this breaks during harmless cleanup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Canvas/Root/Window/Content/Panel/Buttons/Button_01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assert identifiers, components, and visible states that are part of the specification, not child indices that exist only for implementation convenience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wait for completion, not elapsed time
&lt;/h3&gt;

&lt;p&gt;Prefer a completion flag, event, or task over:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WaitForSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keep whole-scene tests rare
&lt;/h3&gt;

&lt;p&gt;Scene tests are valuable but expensive to prepare and diagnose. Build a pyramid of pure logic tests, prefab-level tests, and a small number of scene-level scenarios.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testable design does not mean removing MonoBehaviour
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;MonoBehaviour&lt;/code&gt; is the correct place to receive Unity events, hold Inspector references, and control GameObjects. The problem is placing game rules, storage, networking, time, randomness, and presentation logic in the same component.&lt;/p&gt;

&lt;p&gt;A useful division is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PlayerPresenter : MonoBehaviour
├── receives input and lifecycle events
├── updates the View
└── calls PlayerUseCase

PlayerUseCase : Pure C#
├── damage and item rules
├── state transitions
└── calls repositories
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cover &lt;code&gt;PlayerUseCase&lt;/code&gt; with many fast EditMode tests. Give &lt;code&gt;PlayerPresenter&lt;/code&gt; a small number of tests that verify wiring between Unity events, the use case, and the view.&lt;/p&gt;

&lt;p&gt;A dependency-injection container is not required. Constructors, initialization methods, serialized references, or factories are sufficient when they make dependencies explicit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduce tests into an existing project incrementally
&lt;/h2&gt;

&lt;p&gt;Do not begin by redesigning the entire codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Select frequently changed, high-impact rules
&lt;/h3&gt;

&lt;p&gt;Good first targets include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;paid-item grants&lt;/li&gt;
&lt;li&gt;save-data migration&lt;/li&gt;
&lt;li&gt;daily reset logic&lt;/li&gt;
&lt;li&gt;stamina recovery&lt;/li&gt;
&lt;li&gt;damage formulas&lt;/li&gt;
&lt;li&gt;reward selection&lt;/li&gt;
&lt;li&gt;API response conversion&lt;/li&gt;
&lt;li&gt;master-data validation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Add characterization tests
&lt;/h3&gt;

&lt;p&gt;When the intended specification is unclear, record the current input and output before refactoring. A characterization test does not claim that the existing implementation is ideal. It prevents accidental behavior changes while you improve it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Cut one Unity-dependent boundary
&lt;/h3&gt;

&lt;p&gt;Extract the dependency that blocks testing most: time, randomness, networking, or file I/O. There is no need to convert the entire project to a new architecture at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Add a regression test with each bug fix
&lt;/h3&gt;

&lt;p&gt;When possible:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;reproduce the bug with a failing test&lt;/li&gt;
&lt;li&gt;fix the code&lt;/li&gt;
&lt;li&gt;confirm that the test passes&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  5. Standardize new and modified code first
&lt;/h3&gt;

&lt;p&gt;A rule that new features and changed high-risk code receive tests is more sustainable than trying to cover every legacy class immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decide what not to test
&lt;/h2&gt;

&lt;p&gt;More tests do not automatically mean higher quality.&lt;/p&gt;

&lt;p&gt;Do not retest Unity's own APIs or trivial properties without custom rules. Pixel-perfect visuals, animation appeal, input feel, thermal behavior, and device memory need QA, screenshots, performance tests, or device runs. Avoid detailed tests for short-lived prototypes, but still cover complex calculations. Derive expectations from specification examples rather than copying the production formula into the test.&lt;/p&gt;




&lt;h2&gt;
  
  
  Run tests in CI
&lt;/h2&gt;

&lt;p&gt;Once tests matter, manual execution is not enough.&lt;/p&gt;

&lt;p&gt;A conceptual Windows command for EditMode tests is shown below. The example assumes that CI provides the path to the project-pinned Unity Editor through an environment variable named &lt;code&gt;UNITY_EDITOR_PATH&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$UnityPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;UNITY_EDITOR_PATH&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$UnityPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-or&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$UnityPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UNITY_EDITOR_PATH is not configured correctly."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$UnityPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nt"&gt;-runTests&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nt"&gt;-batchmode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nt"&gt;-projectPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\Projects\MyGame"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nt"&gt;-testPlatform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;EditMode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-testResults&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\TestResults\editmode-results.xml"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-logFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\TestResults\editmode-editor.log"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run PlayMode separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$UnityPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nt"&gt;-runTests&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nt"&gt;-batchmode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nt"&gt;-projectPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\Projects\MyGame"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nt"&gt;-testPlatform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PlayMode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-testResults&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\TestResults\playmode-results.xml"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-logFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\TestResults\playmode-editor.log"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UNITY_EDITOR_PATH&lt;/code&gt; is not a Unity-reserved name. It is only an example of how to provide the exact Editor executable chosen by your project.&lt;/p&gt;

&lt;p&gt;At minimum, retain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;test-result XML&lt;/li&gt;
&lt;li&gt;Editor log&lt;/li&gt;
&lt;li&gt;Unity process exit code&lt;/li&gt;
&lt;li&gt;Unity Editor version&lt;/li&gt;
&lt;li&gt;branch and commit hash&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Layer the pipeline: pull requests run compilation, fast EditMode tests, critical validation, and a small PlayMode smoke suite; main-branch builds run the full Editor suite and asset checks; nightly or release jobs run built-Player, target-platform, long-scenario, and performance tests. Do not put every slow test on every pull request.&lt;/p&gt;

&lt;p&gt;Also avoid adding &lt;code&gt;-nographics&lt;/code&gt; blindly when tests require rendering or a GPU-dependent path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Use categories as CI execution units
&lt;/h2&gt;

&lt;p&gt;Categories are useful only when they are connected to actual workflows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Fast"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Calculate_ReturnsExpected&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AssetValidation"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ValidateAllAssets&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Integration"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UnityTest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;LoginFlow_Completes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the &lt;code&gt;Fast&lt;/code&gt; EditMode category in CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$UnityPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nt"&gt;-runTests&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nt"&gt;-batchmode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nt"&gt;-projectPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\Projects\MyGame"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nt"&gt;-testPlatform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;EditMode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-testCategory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Fast"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-testResults&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\TestResults\fast-editmode-results.xml"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-logFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\TestResults\fast-editmode-editor.log"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unity Test Framework also supports filtering by test assembly with &lt;code&gt;-assemblyNames&lt;/code&gt;. Check the command-line reference for the exact syntax supported by your Unity version, including multiple categories and exclusions.&lt;/p&gt;

&lt;p&gt;Keep the category list small and aligned with pipeline stages. Examples include &lt;code&gt;Fast&lt;/code&gt;, &lt;code&gt;Integration&lt;/code&gt;, &lt;code&gt;Scene&lt;/code&gt;, &lt;code&gt;AssetValidation&lt;/code&gt;, &lt;code&gt;RequiresGraphics&lt;/code&gt;, &lt;code&gt;RequiresDevice&lt;/code&gt;, and &lt;code&gt;Performance&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Do not leave &lt;code&gt;[Ignore]&lt;/code&gt; without a reason. Record the issue, reason, and condition for re-enabling the test.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do not optimize for 100% coverage
&lt;/h2&gt;

&lt;p&gt;Coverage shows which lines ran, not whether important behavior was asserted. Prioritize rules such as preventing duplicate paid-item grants, preserving save data during migration, enforcing daily-reward and currency caps, and avoiding duplicate requests during retries. Use coverage to find suspicious gaps, not as the sole performance target.&lt;/p&gt;




&lt;h2&gt;
  
  
  Monitor test duration
&lt;/h2&gt;

&lt;p&gt;Developers stop running slow suites. Watch for EditMode tests that take seconds, repeated full AssetDatabase scans, unnecessary scene loads, real services, arbitrary waits, and expensive setup. Separate fast and slow suites, record duration in CI, and never improve speed by sharing mutable state between tests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common failure patterns
&lt;/h2&gt;

&lt;p&gt;Avoid five recurring mistakes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not write every test in PlayMode; keep game rules in pure C# where possible.&lt;/li&gt;
&lt;li&gt;Do not add test-only branches to production code; inject alternate dependencies instead.&lt;/li&gt;
&lt;li&gt;Do not depend on the scene that happened to be open when Test Runner started.&lt;/li&gt;
&lt;li&gt;Do not hide unexpected errors with broad &lt;code&gt;LogAssert.ignoreFailingMessages&lt;/code&gt; or retries.&lt;/li&gt;
&lt;li&gt;Review test code for weak assertions, arbitrary waits, and cleanup leaks.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A minimal team policy
&lt;/h2&gt;

&lt;p&gt;A small rule set is enough to begin:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prefer EditMode for game rules and use &lt;code&gt;[UnityTest]&lt;/code&gt; only for frame progression.&lt;/li&gt;
&lt;li&gt;Make tests independent and clean up objects, assets, events, and global state.&lt;/li&gt;
&lt;li&gt;Replace real time, real networks, current time, and uncontrolled randomness with explicit dependencies.&lt;/li&gt;
&lt;li&gt;Add regression tests for important bug fixes.&lt;/li&gt;
&lt;li&gt;Run a fast suite on pull requests and record why any test is ignored.&lt;/li&gt;
&lt;li&gt;Explicitly decide which concerns remain in QA, device, or performance testing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Measure whether tests reduce regression risk and verification time, not merely how many tests exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recommended adoption priority
&lt;/h2&gt;

&lt;p&gt;Start with save migration, paid-item grants, response conversion, date rules, reward and currency calculations, caps, duplicate IDs, and recurring bugs. Next, add prefab, scene, Addressables, localization, presenter, state-machine, and analytics validation. Visual appeal, input feel, GPU behavior, device heat, memory pressure, long sessions, and multi-device communication need QA, performance, screenshot, or scenario testing rather than ordinary unit tests.&lt;/p&gt;

&lt;p&gt;Unreal Engine uses a different stack—Automation Framework, Automation Spec, Functional Testing, and Gauntlet—so it is better treated in a separate article.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Unity testing is not about reproducing the entire game in every test. Cover game rules and data transformations with fast EditMode tests, validate assets with focused editor checks, and use PlayMode only where lifecycle events, frames, scenes, or physics are part of the behavior.&lt;/p&gt;

&lt;p&gt;For asynchronous work, inject external services, clocks, randomness, storage, and cancellation so tests can observe completion without real waiting. Existing projects can start with high-impact rules, recurring bugs, save migration, and static asset validation instead of an immediate rewrite.&lt;/p&gt;

&lt;p&gt;The useful metric is whether the team can change the game and verify critical behavior quickly. Unity Test Framework is a programmer's safety net, not a replacement for QA.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/test-framework/test-framework-introduction.html" rel="noopener noreferrer"&gt;Unity Manual: Testing your code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/test-framework/edit-mode-vs-play-mode-tests.html" rel="noopener noreferrer"&gt;Unity Manual: Edit mode and Play mode tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/test-framework/workflow-create-test-assembly.html" rel="noopener noreferrer"&gt;Unity Manual: Create a test assembly&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/test-framework/workflow-create-test.html" rel="noopener noreferrer"&gt;Unity Manual: Create a test&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/test-framework/reference-async-tests.html" rel="noopener noreferrer"&gt;Unity Manual: Asynchronous tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/async-awaitable-introduction.html" rel="noopener noreferrer"&gt;Unity Manual: Introduction to asynchronous programming with Awaitable&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/async-awaitable-continuations.html" rel="noopener noreferrer"&gt;Unity Manual: Awaitable completion and continuation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/ScriptReference/GameObjectUtility.GetMonoBehavioursWithMissingScriptCount.html" rel="noopener noreferrer"&gt;Unity Scripting API: GameObjectUtility.GetMonoBehavioursWithMissingScriptCount&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.test-framework@1.6/api/UnityEngine.TestTools.UnityTearDownAttribute.html" rel="noopener noreferrer"&gt;Unity Test Framework: UnityTearDownAttribute&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Cysharp/UniTask" rel="noopener noreferrer"&gt;UniTask&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/test-framework/running-tests.html" rel="noopener noreferrer"&gt;Unity Manual: Running tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/test-framework/run-tests-from-command-line.html" rel="noopener noreferrer"&gt;Unity Manual: Run tests from the command line&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/test-framework/reference-command-line.html" rel="noopener noreferrer"&gt;Unity Manual: Command-line reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.test-framework@1.6/api/UnityEngine.TestTools.LogAssert.html" rel="noopener noreferrer"&gt;Unity Test Framework: LogAssert&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.test-framework@1.6/changelog/CHANGELOG.html" rel="noopener noreferrer"&gt;Unity Test Framework 1.6 changelog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>testing</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Using Unreal MCP in UE 5.8 from Codex: Setup, Toolsets, and Safe Editor Automation</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Tue, 28 Jul 2026 05:30:59 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/using-unreal-mcp-in-ue-58-from-codex-setup-toolsets-and-safe-editor-automation-4pon</link>
      <guid>https://dev.to/gamedevtoollab/using-unreal-mcp-in-ue-58-from-codex-setup-toolsets-and-safe-editor-automation-4pon</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Unreal Engine 5.8 adds &lt;strong&gt;Unreal MCP&lt;/strong&gt;, an experimental plugin that exposes Unreal Editor operations to AI agents through the Model Context Protocol. Once Codex is connected, it can inspect or edit actors, configure lighting, create material instances, run automation tests, and invoke project-specific editor tools from natural language.&lt;/p&gt;

&lt;p&gt;That does &lt;strong&gt;not&lt;/strong&gt; mean you should hand a production project to Codex and hope it figures everything out.&lt;/p&gt;

&lt;p&gt;In UE 5.8, Unreal MCP is still experimental. Epic Games notes that APIs and data formats may change. Its HTTP server has no authentication layer, and tool calls are executed serially on Unreal Engine's game thread. Without guardrails, an agent can edit the wrong level, save an asset to the wrong folder, duplicate an object after a timeout, or conflict with another connected client.&lt;/p&gt;

&lt;p&gt;This guide builds the integration in stages: understand the architecture, connect Codex, verify reads, allow one sandboxed write, then add approval, recovery, source-control, and custom-tool rules.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Version note:&lt;/strong&gt; This article is based on UE 5.8 and Codex documentation checked on July 21, 2026. It is an implementation and operations guide, not a repeated benchmark. Verify the plugin settings, Output Log, and actual schemas in your installed build before adoption.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Understand the three Unreal-side layers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Codex CLI / Codex IDE extension
        │ MCP over Streamable HTTP
        ▼
http://127.0.0.1:8000/mcp
        │
        ▼
Unreal Editor
  ├─ Unreal MCP (ModelContextProtocol)
  │    └─ Server, connection, Tool Search
  ├─ Toolset Registry
  │    └─ Toolset discovery and schemas
  └─ AllToolsets / individual toolsets
       └─ Actor, scene, material, and other operations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Plugin Browser name is &lt;strong&gt;Unreal MCP&lt;/strong&gt;; its identifier and console-command prefix are &lt;code&gt;ModelContextProtocol&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Unreal MCP mainly provides the server and protocol endpoint. The Toolset Registry discovers executable operations and generates schemas. &lt;code&gt;AllToolsets&lt;/code&gt; enables the toolsets bundled with UE 5.8.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Enabled state&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unreal MCP only&lt;/td&gt;
&lt;td&gt;The server exists, but expected editor tools may be absent.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toolset Registry&lt;/td&gt;
&lt;td&gt;Unreal can discover toolsets; it is enabled as a dependency.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AllToolsets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bundled UE 5.8 toolsets are enabled together.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Selected toolsets only&lt;/td&gt;
&lt;td&gt;Smaller exposure, but you manage features and dependencies.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use &lt;code&gt;AllToolsets&lt;/code&gt; for initial connectivity testing. For regular work, disable unnecessary toolsets and narrow permissions on both Unreal and Codex.&lt;/p&gt;

&lt;p&gt;Unreal MCP can expose actor editing, lighting, material-instance creation, Slate inspection, automation tests, and custom project tools. It is not a UI macro. Codex can call only tools published by the server, and vague instructions do not reliably identify the correct asset, level, coordinate space, save location, or overwrite policy.&lt;/p&gt;

&lt;p&gt;UE 5.8's bundled implementation focuses on MCP &lt;strong&gt;Tools&lt;/strong&gt; rather than Resources or Prompts. A connected server showing no Resources is not necessarily broken. Tool calls also run serially on the game thread, so do not let multiple Codex sessions or MCP clients issue overlapping writes to one editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prepare a disposable sandbox
&lt;/h2&gt;

&lt;p&gt;Do not begin in a production map. Use:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a UE 5.8 test project or production-project copy;&lt;/li&gt;
&lt;li&gt;a dedicated source-control branch;&lt;/li&gt;
&lt;li&gt;a level such as &lt;code&gt;MCP_Sandbox&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a folder such as &lt;code&gt;/Game/MCPTest&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Codex CLI or the Codex IDE extension;&lt;/li&gt;
&lt;li&gt;a commit or checkpoint before the first write.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep the level simple. A floor, one light, and one static mesh are enough. Avoid World Partition, active Sequencer work, or anything that makes diffs harder to understand.&lt;/p&gt;

&lt;p&gt;Before expanding the scope, prove that Codex connects to the intended editor, reads the selected actor accurately, and discovers tool schemas. It must also limit writes to the requested level and folder, re-read saved results, and stop on unexpected state. Check for actors left in another level, duplicate assets, extra components, and unsaved packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install Codex
&lt;/h2&gt;

&lt;p&gt;Skip this section if Codex CLI is already available.&lt;/p&gt;

&lt;p&gt;Windows PowerShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;powershell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExecutionPolicy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ByPass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"irm https://chatgpt.com/codex/install.ps1 | iex"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;macOS or Linux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://chatgpt.com/codex/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node.js and npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @openai/codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first two are official installation paths but execute a remote script directly. If pipe-to-shell installation is prohibited, inspect the script first, use npm or Homebrew, install an &lt;a href="https://github.com/openai/codex/releases" rel="noopener noreferrer"&gt;official GitHub release&lt;/a&gt;, or follow your internal distribution process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure Unreal Editor
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;Edit &amp;gt; Plugins&lt;/code&gt;, enable &lt;strong&gt;Unreal MCP&lt;/strong&gt;, and restart when prompted. The Toolset Registry dependency should be enabled automatically. Then enable &lt;strong&gt;AllToolsets&lt;/strong&gt; and restart again if required.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;Edit &amp;gt; Editor Preferences &amp;gt; General &amp;gt; Model Context Protocol&lt;/code&gt;. The UE 5.8 documentation lists these defaults:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auto Start Server&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server Port Number&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server URL Path&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/mcp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable Tool Search&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After an engine update, treat the values shown by your editor and Output Log as authoritative.&lt;/p&gt;

&lt;p&gt;Keep Auto Start disabled during early testing and start the server manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ModelContextProtocol.StartServer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To specify a port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ModelContextProtocol.StartServer 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default endpoint is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://127.0.0.1:8000/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Search Output Log for &lt;code&gt;LogModelContextProtocol&lt;/code&gt; and confirm the bind address, port, and path. Stop the server when finished:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ModelContextProtocol.StopServer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server has no authentication layer, so leaving it running indefinitely is a poor default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate the Codex configuration
&lt;/h2&gt;

&lt;p&gt;Run this in the Unreal Editor console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ModelContextProtocol.GenerateClientConfig Codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codex uses &lt;code&gt;~/.codex/config.toml&lt;/code&gt; for user-level settings and can read &lt;code&gt;.codex/config.toml&lt;/code&gt; from a trusted project. Check Output Log for the generated destination.&lt;/p&gt;

&lt;p&gt;The Codex TOML generation step is effectively &lt;strong&gt;write-once&lt;/strong&gt;: the generator does not overwrite an existing TOML. The file itself remains editable. If other MCP servers are already configured, merge the Unreal entry manually.&lt;/p&gt;

&lt;p&gt;UE 5.8's Unreal MCP uses HTTP and Server-Sent Events, not &lt;code&gt;stdio&lt;/code&gt; or WebSocket transport. Register a Streamable HTTP &lt;code&gt;url&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[mcp_servers.unreal-mcp]&lt;/span&gt;
&lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://127.0.0.1:8000/mcp"&lt;/span&gt;
&lt;span class="py"&gt;required&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;tool_timeout_sec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;
&lt;span class="py"&gt;default_tools_approval_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"prompt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;required = false&lt;/code&gt; prevents an unavailable editor from blocking unrelated Codex work. The default tool timeout is 60 seconds; &lt;code&gt;120&lt;/code&gt; is only an initial adjustment for operations that consistently exceed it. After a timeout, inspect the editor before retrying because an operation may have completed or partially changed state.&lt;/p&gt;

&lt;p&gt;Keep &lt;code&gt;default_tools_approval_mode = "prompt"&lt;/code&gt; while learning the exposed schemas. The &lt;code&gt;writes&lt;/code&gt; mode allows tools marked read-only and prompts for others, but Unreal's default Tool Search funnels inner operations through &lt;code&gt;call_tool&lt;/code&gt;. Codex may not classify each inner operation independently, so &lt;code&gt;writes&lt;/code&gt; is not automatically a fine-grained safety boundary.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Codex's filesystem sandbox and MCP operations inside Unreal Editor are different boundaries. Limiting workspace writes does not necessarily stop an approved MCP tool from changing a level or asset. Combine approval, limited toolsets, sandbox levels, dedicated content folders, and source control.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Start Codex from the project root
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\Work\Unreal\MCPExample&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;codex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Codex TUI, inspect servers active in the current session with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From a terminal, list configured servers with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;codex&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;codex mcp list&lt;/code&gt; confirms configuration registration; it does not prove the Unreal endpoint is reachable. &lt;code&gt;/mcp&lt;/code&gt; shows the current-session state. End-to-end verification requires a response from &lt;code&gt;list_toolsets&lt;/code&gt; or &lt;code&gt;describe_toolset&lt;/code&gt;, or a successful MCP Inspector connection.&lt;/p&gt;

&lt;p&gt;Project-local &lt;code&gt;.codex/config.toml&lt;/code&gt; is loaded only for a trusted project. If a user-level entry works but the project entry does not, check the launch directory and trust state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why only three tools may be visible
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;Enable Tool Search&lt;/code&gt; enabled, Unreal MCP initially exposes three meta-tools instead of hundreds of schemas:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_toolsets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lists available toolsets.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;describe_toolset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns schemas inside a toolset.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;call_tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Invokes an inner tool by toolset, name, and arguments.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are the UE 5.8 documented names; confirm them with &lt;code&gt;/mcp&lt;/code&gt; or MCP Inspector after an update.&lt;/p&gt;

&lt;p&gt;This design affects allowlists. Codex's &lt;code&gt;enabled_tools&lt;/code&gt; filters names exposed by the server. When Tool Search collapses operations behind &lt;code&gt;call_tool&lt;/code&gt;, allowing that outer tool does not create a separate permission rule for every inner tool.&lt;/p&gt;

&lt;p&gt;For schema discovery only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[mcp_servers.unreal-mcp]&lt;/span&gt;
&lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://127.0.0.1:8000/mcp"&lt;/span&gt;
&lt;span class="py"&gt;enabled_tools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"list_toolsets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"describe_toolset"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;default_tools_approval_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"prompt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add execution only when ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;enabled_tools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"list_toolsets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"describe_toolset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"call_tool"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep approval enabled and reduce active Unreal toolsets. Disabling Tool Search exposes individual tools and permits a more precise Codex allowlist, but increases the initial schema payload and ongoing maintenance for renamed schemas, stale allowlists, and accidental over-permission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify reads before writes
&lt;/h2&gt;

&lt;p&gt;First, discover schemas without invoking an inner operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check the Unreal MCP connection without changing any editor state.

1. Use list_toolsets to list available toolsets.
2. Identify candidates for actors, scenes, and material instances.
3. Use describe_toolset and separate read operations from writes.
4. Do not invoke call_tool. Report only the tools and arguments you would use next.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After review, add &lt;code&gt;call_tool&lt;/code&gt; to &lt;code&gt;enabled_tools&lt;/code&gt;, restart Codex, select a cube in &lt;code&gt;MCP_Sandbox&lt;/code&gt;, and request a read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read the actor currently selected in Unreal Editor.
Report Actor Label, Class, Object Path, world Transform, and owning Level.
Do not modify, save, or change the selection.
Inspect the schema first, then perform only the read operation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare the response with the editor. Move the cube and read again to detect stale data. A successful check identifies the intended editor state, distinguishes Actor Label from Object Path, reports the correct world transform, causes no side effects, and names the toolset and tool used.&lt;/p&gt;

&lt;p&gt;Do not proceed to writes until this is reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the first write small and observable
&lt;/h2&gt;

&lt;p&gt;Add one point light to the sandbox rather than generating a complex Blueprint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use Unreal MCP to edit only the open MCP_Sandbox level.

Goal:
- Add one Point Light named MCP_TestPointLight.
- Location: X=0, Y=0, Z=300.
- Intensity: 5000.

Rules:
- Show the toolset, tool, arguments, and target before execution.
- Wait for approval before call_tool.
- If the actor already exists, stop instead of duplicating it.
- Do not modify another level, asset, or Project Setting.
- Re-read the result before saving.
- Never use Save All; save only the target level.
- Re-read the Object Path and values after saving.
- Stop if any unexpected actor or component appears.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The completion criterion is not a confident chat message. Use this sequence: &lt;strong&gt;plan and approve → execute one operation → re-read → save only the target package → re-read again → inspect the editor and source-control diff&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Do not split writes across multiple Codex subagents. A second agent can invalidate the assumptions of an already approved plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix material and test inputs explicitly
&lt;/h2&gt;

&lt;p&gt;For a Material Instance, define the parent, destination, asset name, allowed parameters, overwrite behavior, and verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create one Material Instance through Unreal MCP.

Parent: /Game/MCPTest/Materials/M_Master
Output: /Game/MCPTest/Materials/Instances/MI_MCP_Red
BaseColor: R=1, G=0, B=0, A=1
Roughness: 0.6

Stop if the asset already exists, the output folder is missing, or either parameter is absent.
Do not modify the parent or anything outside /Game/MCPTest.
Inspect the tool schema first. After creation and after saving, re-read the parent,
parameter values, and Object Path. Save only the new asset.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never leave overwrite policy to the tone of natural language. Choose stop, another name, or explicit approval.&lt;/p&gt;

&lt;p&gt;Automation Tests are often a lower-risk starting point, although tests can still load maps or create assets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run only the Automation Test Project.MCP.Smoke.
Confirm an exact name match first.
Do not modify code, Blueprints, or config, and do not auto-fix failures.
Report the result, failed test names, important log lines, and duration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep observation and modification in separate turns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put persistent rules in AGENTS.md
&lt;/h2&gt;

&lt;p&gt;Store repeated project rules in &lt;code&gt;AGENTS.md&lt;/code&gt; at the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Unreal MCP Working Agreement&lt;/span&gt;

&lt;span class="gu"&gt;## Scope&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Writes are limited to &lt;span class="sb"&gt;`/Game/MCPTest`&lt;/span&gt; and &lt;span class="sb"&gt;`MCP_Sandbox`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Do not modify other levels, assets, plugins, or Project Settings.

&lt;span class="gu"&gt;## Workflow&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Inspect schemas before execution.
&lt;span class="p"&gt;-&lt;/span&gt; Show toolset, tool, arguments, and targets before every write.
&lt;span class="p"&gt;-&lt;/span&gt; Execute &lt;span class="sb"&gt;`call_tool`&lt;/span&gt; one at a time; never parallelize writes.
&lt;span class="p"&gt;-&lt;/span&gt; After a timeout, re-read state instead of resending the write.
&lt;span class="p"&gt;-&lt;/span&gt; Check for duplicates before creation.
&lt;span class="p"&gt;-&lt;/span&gt; Re-read after creation and after saving.
&lt;span class="p"&gt;-&lt;/span&gt; Never use &lt;span class="sb"&gt;`Save All`&lt;/span&gt;.

&lt;span class="gu"&gt;## Stop conditions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; The target is outside the allowed scope.
&lt;span class="p"&gt;-&lt;/span&gt; An existing asset has no overwrite policy.
&lt;span class="p"&gt;-&lt;/span&gt; The schema changed, the editor is unresponsive, or an unexpected diff appears.
&lt;span class="p"&gt;-&lt;/span&gt; The target editor instance cannot be identified unambiguously.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; is not an authorization system. Continue using MCP approval, limited toolsets, source control, and the sandbox. Start a new project-root session after changing it when you need the new instructions loaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting and MCP Inspector
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;unreal-mcp&lt;/code&gt; does not appear or respond, check in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start the server and confirm its endpoint in Output Log.&lt;/li&gt;
&lt;li&gt;Compare that endpoint with &lt;code&gt;.codex/config.toml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Confirm the project root and trust state.&lt;/li&gt;
&lt;li&gt;Restart Codex; use &lt;code&gt;codex mcp list&lt;/code&gt; for registration.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;/mcp&lt;/code&gt; for current-session state.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;list_toolsets&lt;/code&gt; or connect with MCP Inspector for live traffic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only seeing the three meta-tools is normal with Tool Search enabled. If &lt;code&gt;list_toolsets&lt;/code&gt; is empty, confirm Unreal MCP, Toolset Registry, and &lt;code&gt;AllToolsets&lt;/code&gt;, then restart the editor.&lt;/p&gt;

&lt;p&gt;After adding or changing a toolset, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ModelContextProtocol.RefreshTools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reconnect Codex afterward. A new C++ &lt;code&gt;UFUNCTION&lt;/code&gt; declaration may require an editor restart rather than Live Coding alone.&lt;/p&gt;

&lt;p&gt;If the port changes, update both Unreal and Codex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[mcp_servers.unreal-mcp]&lt;/span&gt;
&lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://127.0.0.1:8123/mcp"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A timeout does not prove nothing happened. Do not resend the same write. Check editor responsiveness, Output Log, the target object, and source-control diff, then plan only the remaining work.&lt;/p&gt;

&lt;p&gt;To separate Codex issues from server issues, run MCP Inspector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @modelcontextprotocol/inspector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run may download a package and require Node.js, npm, network access, and execution approval. Choose Streamable HTTP and connect to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://127.0.0.1:8000/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspector shows exposed tools, schemas, argument forms, and protocol errors. It can also execute tools, so prefer discovery and reads while troubleshooting. If Inspector fails too, investigate Unreal. If Inspector works but Codex fails, focus on Codex configuration, approval, or prompt interpretation.&lt;/p&gt;

&lt;p&gt;For more Unreal-side detail, temporarily use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Log LogModelContextProtocol Verbose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restore normal verbosity afterward and treat project logs as potentially sensitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Localhost is not authentication
&lt;/h2&gt;

&lt;p&gt;Unreal MCP binds to loopback by default and rejects non-loopback origins, but it has no authentication layer. Do not casually bind it to &lt;code&gt;0.0.0.0&lt;/code&gt;, forward the port for remote use, expose it to a LAN without authentication, let several people control one editor, or use it as a production-build endpoint.&lt;/p&gt;

&lt;p&gt;A VPN, office LAN, or localhost proxy is not a substitute for authentication. This article assumes Codex and Unreal Editor run on the same developer machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repeated work belongs in narrow custom tools
&lt;/h2&gt;

&lt;p&gt;Generic actor and scene tools are useful for exploration. Repeated workflows are safer as project-specific operations with fewer valid failure modes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CreateValidatedEnemyActor(
  enemyDefinition,
  targetLevel,
  transform,
  outputFolder,
  overwritePolicy,
  dryRun
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation should validate scope and duplicates, return planned changes from &lt;code&gt;dryRun&lt;/code&gt;, and verify source-control checkout or locks. It should group undoable editor changes in a transaction, run post-validation, track dirty packages, save only intended packages, and return structured state before and after saving.&lt;/p&gt;

&lt;p&gt;MCP does not automatically wrap a tool call in an Unreal transaction. C++ &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Editor/UnrealEd/FScopedTransaction" rel="noopener noreferrer"&gt;&lt;code&gt;FScopedTransaction&lt;/code&gt;&lt;/a&gt; and Python &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/scripting-the-unreal-editor-using-python" rel="noopener noreferrer"&gt;&lt;code&gt;unreal.ScopedEditorTransaction&lt;/code&gt;&lt;/a&gt; help only with operations the editor can undo. Imports, saved packages, external files, and source-control state may need explicit compensation and checkpoints.&lt;/p&gt;

&lt;p&gt;A practical flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dryRun
→ validate input, permissions, and source control
→ execute undoable work in a transaction
→ post-validate
→ save target packages only
→ re-read state
→ return a structured result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On failure, report separately what was undone, cleaned up, and left for a human. In UE 5.8, custom toolsets can derive from &lt;code&gt;unreal.ToolsetDefinition&lt;/code&gt; in Python or &lt;code&gt;UToolsetDefinition&lt;/code&gt; in C++. After adding one, run &lt;code&gt;ModelContextProtocol.RefreshTools&lt;/code&gt; and reconnect Codex.&lt;/p&gt;

&lt;p&gt;The value of a custom tool is not merely convenience. Moving natural-language constraints into implementation reduces the choices through which the AI can fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  PCG requires references and a reviewed plan
&lt;/h2&gt;

&lt;p&gt;Epic also documents using Unreal MCP and LLMs with PCG graphs. PCG is a poor first connectivity test because a graph can be technically valid while its data flow is wrong or needlessly complex.&lt;/p&gt;

&lt;p&gt;Some official examples assume Claude Code-specific supporting material. With Codex, place equivalent instructions in a Codex Skill, &lt;code&gt;AGENTS.md&lt;/code&gt;, or a project reference document and require them to be read before planning. Have a human choose the reference graph and assets, ask Codex to explain the intended data flow, review the plan, and generate small sections in stages. A working MCP connection does not imply correct domain design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure safety before adoption
&lt;/h2&gt;

&lt;p&gt;Repeat the same tasks and record first-pass success, human intervention, out-of-scope changes, duplicate creation, and persistence after reload. Also track tool-call count, duration, recovery time, and schema stability after restarts or plugin updates.&lt;/p&gt;

&lt;p&gt;Prioritize zero out-of-scope changes, understandable failures, and reliable recovery over a small speed improvement. Blueprints, materials, PCG graphs, and Sequencer content also require in-editor validation; a text diff alone rarely proves a visual asset is correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A safe UE 5.8 Unreal MCP workflow from Codex is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable Unreal MCP and &lt;code&gt;AllToolsets&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Start the local server with &lt;code&gt;ModelContextProtocol.StartServer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Generate the entry with &lt;code&gt;ModelContextProtocol.GenerateClientConfig Codex&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Launch Codex from the project or workspace root.&lt;/li&gt;
&lt;li&gt;Verify &lt;code&gt;/mcp&lt;/code&gt;, &lt;code&gt;list_toolsets&lt;/code&gt;, and &lt;code&gt;describe_toolset&lt;/code&gt; before writing.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;call_tool&lt;/code&gt; with approval and make one sandboxed change at a time.&lt;/li&gt;
&lt;li&gt;Re-read after saving and inspect source-control diffs.&lt;/li&gt;
&lt;li&gt;Move repeated work into narrow, validated custom tools.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Remember the division of responsibilities: Unreal MCP provides the server, Toolset Registry provides discovery and schemas, and &lt;code&gt;AllToolsets&lt;/code&gt; provides bundled editor operations. Tool Search also funnels operations through &lt;code&gt;call_tool&lt;/code&gt;, so an allowlist around that outer tool is not automatically a fine-grained permission model for every inner operation.&lt;/p&gt;

&lt;p&gt;Treat Unreal MCP as an editor-extension entry point, not natural-language control for everything. Start with reads, expand to bounded sandbox writes, and only then expose reviewed project-specific tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;Verified on July 21, 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-mcp-in-unreal-editor" rel="noopener noreferrer"&gt;Unreal MCP in Unreal Editor | Unreal Engine 5.8 Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-5-8-release-notes" rel="noopener noreferrer"&gt;Unreal Engine 5.8 Release Notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/working-with-pcg-and-llms-using-unreal-mcp-in-unreal-engine" rel="noopener noreferrer"&gt;Working with PCG and LLMs Using Unreal MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/codex/mcp" rel="noopener noreferrer"&gt;Model Context Protocol | Codex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/codex/config-reference" rel="noopener noreferrer"&gt;Codex Configuration Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/codex/agent-configuration/agents-md" rel="noopener noreferrer"&gt;Custom instructions with AGENTS.md | Codex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/codex/agent-approvals-security" rel="noopener noreferrer"&gt;Agent approvals and security | Codex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/codex/cli" rel="noopener noreferrer"&gt;OpenAI Codex CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openai/codex" rel="noopener noreferrer"&gt;openai/codex | GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/docs/tools/inspector" rel="noopener noreferrer"&gt;MCP Inspector | Model Context Protocol&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unrealengine</category>
      <category>gamedev</category>
      <category>ai</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Unity 6.5 DirectStorage: Interpreting an 18.6% Load-Time Reduction and the 'Up to 40%' Claim</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Sun, 26 Jul 2026 00:26:43 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/unity-65-directstorage-interpreting-an-186-load-time-reduction-and-the-up-to-40-claim-2o8e</link>
      <guid>https://dev.to/gamedevtoollab/unity-65-directstorage-interpreting-an-186-load-time-reduction-and-the-up-to-40-claim-2o8e</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Unity introduced DirectStorage support in Unity 6.4 and expanded it in 6.5 so qualifying custom reads through &lt;code&gt;AsyncReadManager&lt;/code&gt; can use the Windows DirectStorage path.&lt;/p&gt;

&lt;p&gt;This makes the feature relevant to local map chunks, master data, and other large custom binaries as well as built-in assets. It does &lt;strong&gt;not&lt;/strong&gt; make network transfer, parsing, object creation, scene activation, or shaders faster. The practical question is which part of a loading pipeline is eligible, how much time that part consumes, and whether the player actually submitted work to a DirectStorage queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Unity 6.4 and 6.5 Changed
&lt;/h2&gt;

&lt;p&gt;Unity 6.4 divided archive work into chunks, submitted lower-level reads in parallel, and moved decompression work into jobs. On Windows, Unity can replace that queue with DirectStorage for Texture, Mesh, and DOTS/ECS Entities asset data.&lt;/p&gt;

&lt;p&gt;Unity 6.5 extends the path to C# reads performed through &lt;code&gt;AsyncReadManager&lt;/code&gt;. Treat the following as minimum conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unity 6.5 and a Windows 64-bit player&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable DirectStorage&lt;/strong&gt; turned on&lt;/li&gt;
&lt;li&gt;A local file read through &lt;code&gt;AsyncReadManager&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A test separated from Editor Play Mode and ordinary .NET file I/O&lt;/li&gt;
&lt;li&gt;PIX evidence that the enabled build uses a DirectStorage queue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;File.ReadAllBytes&lt;/code&gt;, &lt;code&gt;FileStream&lt;/code&gt;, and ordinary file access inside a third-party library do not switch automatically.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Unity 6.5 status&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Texture and Mesh&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;td&gt;Windows player with DirectStorage enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local AssetBundle or Addressables Texture/Mesh&lt;/td&gt;
&lt;td&gt;Supported path&lt;/td&gt;
&lt;td&gt;Only the local-file stage is relevant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOTS Entities asset data&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;td&gt;Explicitly documented by Unity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom &lt;code&gt;AsyncReadManager&lt;/code&gt; reads&lt;/td&gt;
&lt;td&gt;Conditional&lt;/td&gt;
&lt;td&gt;Verify the queue in PIX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;File.ReadAllBytes&lt;/code&gt; / &lt;code&gt;FileStream&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Not automatic&lt;/td&gt;
&lt;td&gt;Requires an I/O redesign&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote Addressables download&lt;/td&gt;
&lt;td&gt;Not covered&lt;/td&gt;
&lt;td&gt;Network transfer is separate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON / MessagePack parsing&lt;/td&gt;
&lt;td&gt;Not covered&lt;/td&gt;
&lt;td&gt;CPU work after reading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Instantiate&lt;/code&gt; / scene activation&lt;/td&gt;
&lt;td&gt;Not covered&lt;/td&gt;
&lt;td&gt;Object creation and initialization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DirectStorage GPU decompression&lt;/td&gt;
&lt;td&gt;Not used&lt;/td&gt;
&lt;td&gt;Unity 6.5 does not enable it automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AudioClip and other undocumented asset types should be treated as unconfirmed and checked with PIX or &lt;code&gt;AsyncReadManagerMetrics&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the Public Benchmark Correctly
&lt;/h2&gt;

&lt;p&gt;A Unity Discussions user published an HDRP test on an SSD rated at roughly 5 GB/s sequential throughput. Later loads were compared to avoid the first PSO warm-up pass.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Reported load time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DirectStorage off&lt;/td&gt;
&lt;td&gt;About 7.49 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DirectStorage on&lt;/td&gt;
&lt;td&gt;About 6.10 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Difference&lt;/td&gt;
&lt;td&gt;1.39 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The elapsed-time reduction is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(7.49 - 6.10) / 7.49 × 100 = 18.56%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The load time therefore fell by approximately &lt;strong&gt;18.6%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The post described the result as about “22% faster.” That uses a rate ratio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;7.49 / 6.10 = 1.2279...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The work completed at about 1.228 times the previous rate, a &lt;strong&gt;22.8% processing-rate increase&lt;/strong&gt;. Both numbers are valid, but “load-time reduction” should use 18.6% and state the denominator.&lt;/p&gt;

&lt;p&gt;The same post showed HWiNFO values of about 15 MB/s off and 530 MB/s on. The poster also said the off value did not match the elapsed time and might be affected by Windows caching. That is not evidence for a “35x faster” claim.&lt;/p&gt;

&lt;p&gt;Another user reported a 40% improvement for a Windows 10 scene containing 17 GB of Texture2D data. It is useful as a lead, not a universal prediction: detailed timings, repetitions, CPU, and SSD information were not provided.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why “Up to 40%” Does Not Mean 40% Faster Total Loading
&lt;/h3&gt;

&lt;p&gt;Unity's figure describes a favorable supported-I/O case. Total loading can also contain unsupported reads, decompression, deserialization, object creation, shader work, and scene activation.&lt;/p&gt;

&lt;p&gt;Let &lt;code&gt;p&lt;/code&gt; be the fraction of total time spent in eligible I/O and &lt;code&gt;r&lt;/code&gt; the reduction in that section. The approximate total reduction is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;p × r
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If only half of a 10-second load is eligible and that half improves by 40%, the total becomes about 8 seconds: a 20% reduction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable DirectStorage and Control the Build
&lt;/h2&gt;

&lt;p&gt;The setting is available at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Edit
  &amp;gt; Project Settings
    &amp;gt; Player
      &amp;gt; Other Settings
        &amp;gt; Configuration
          &amp;gt; Enable DirectStorage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unity 6.5 also exposes &lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/PlayerSettings-enableDirectStorage.html" rel="noopener noreferrer"&gt;&lt;code&gt;PlayerSettings.enableDirectStorage&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;PlayerSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enableDirectStorage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unity issue &lt;a href="https://issuetracker.unity3d.com/issues/enabling-slash-disabling-direct-storage-on-windows-default-build-profile-is-broken" rel="noopener noreferrer"&gt;UUM-133978&lt;/a&gt; affected prerelease 6.4/6.5 versions. Unity lists fixes in 6000.4.1f1 and 6000.5.0a9, and the 6000.5.0f1 notes describe Project Settings, version-control, and C# support. Projects upgraded from the older storage location can start with the setting disabled, so recheck it.&lt;/p&gt;

&lt;p&gt;A custom Build Profile can override global Player settings. The following sample intentionally requires the standard &lt;strong&gt;Platforms &amp;gt; Windows&lt;/strong&gt; profile.&lt;/p&gt;

&lt;p&gt;Place it in &lt;code&gt;Assets/Editor/DirectStorageBuildMenu.cs&lt;/code&gt; or an Editor-only assembly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#if UNITY_EDITOR
&lt;/span&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.IO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Linq&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEditor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEditor.Build.Profile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEditor.Build.Reporting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DirectStorageBuildMenu&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;MenuItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Build/Windows/DirectStorage A-B"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;BuildBoth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;ValidateBuildContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PlayerSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enableDirectStorage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Builds/DS-Off/Game.exe"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s"&gt;"Builds/DS-On/Game.exe"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;PlayerSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enableDirectStorage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ValidateBuildContext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EditorUserBuildSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activeBuildTarget&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt;
            &lt;span class="n"&gt;BuildTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StandaloneWindows64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Activate the Windows 64-bit target before running this menu item."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetActiveBuildProfile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"This sample expects the standard Platforms &amp;gt; Windows profile."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;scenes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EditorBuildSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scenes&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No enabled scenes were found."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDirectoryName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;PlayerSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enableDirectStorage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;$"Build DS=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PlayerSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enableDirectStorage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, Unity=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unityVersion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, Output=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;BuildReport&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BuildPipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BuildPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;BuildPlayerOptions&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;scenes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scenes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;locationPathName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BuildTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StandaloneWindows64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BuildOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;BuildResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Succeeded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;$"Build failed: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script restores the original setting and fails instead of silently switching targets. Folder names and log lines are labels, not proof. Only the enabled build should show a DirectStorage queue in PIX.&lt;/p&gt;

&lt;p&gt;Keep the Unity patch, packages, scripting backend, Graphics API, content, and every other build option identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure a Reproducible Windows Player
&lt;/h2&gt;

&lt;p&gt;Editor Play Mode includes AssetDatabase behavior and persistent Editor caches, so use Windows player builds.&lt;/p&gt;

&lt;p&gt;First run a stress test with large supported assets or a custom binary, then measure the real startup, transition, or streaming path. If only the stress test improves, investigate parsing, object creation, activation, and main-thread work. If neither improves, investigate the setting, target types, caching, OS, and PIX queue.&lt;/p&gt;

&lt;p&gt;Unity's AssetBundle guide states that &lt;code&gt;LoadFromFile&lt;/code&gt; and &lt;code&gt;LoadFromFileAsync&lt;/code&gt; open LZMA bundles through conversion to an LZ4 in-memory file. That adds whole-bundle and memory effects, so validate the DirectStorage path first with LZ4 or an uncompressed bundle. Treat a production LZMA download/cache workflow as a separate condition.&lt;/p&gt;

&lt;p&gt;Do not mix cold and warm runs. Alternate off and on builds for at least 10 runs; 20 to 30 is better for a production decision. Keep the median and p95, and record Unity version, Git commit, Windows version, CPU, storage, drivers, BitLocker, Graphics API, scripting backend, compression, and run number.&lt;/p&gt;

&lt;h3&gt;
  
  
  Record AssetBundle Timing to CSV
&lt;/h3&gt;

&lt;p&gt;The following stress test mounts a local AssetBundle and loads every asset. Place the Windows bundle at &lt;code&gt;Assets/StreamingAssets/ds_test&lt;/code&gt; so it is included in the player.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Diagnostics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.IO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Debug&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BundleBenchmark&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CsvHeader&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="s"&gt;"utc,unity,condition,runIndex,mountMs,assetsMs,totalMs,assetCount"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;bundleName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;runIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;bundleName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-bundleName"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ds_test"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-condition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Unknown"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-runIndex"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;runIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;streamingAssetsPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bundleName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Bundle not found: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;Quit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;Stopwatch&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartNew&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;Stopwatch&lt;/span&gt; &lt;span class="n"&gt;mount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartNew&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;AssetBundleCreateRequest&lt;/span&gt; &lt;span class="n"&gt;open&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AssetBundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadFromFileAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assetBundle&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Load failed: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;Quit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;AssetBundle&lt;/span&gt; &lt;span class="n"&gt;bundle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assetBundle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Stopwatch&lt;/span&gt; &lt;span class="n"&gt;assets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartNew&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;AssetBundleRequest&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadAllAssetsAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FormattableString&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Invariant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;O&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unityVersion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;runIndex&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Elapsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalMilliseconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;F3&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Elapsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalMilliseconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;F3&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Elapsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalMilliseconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;F3&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allAssets&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;persistentDataPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ds_results.csv"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;try&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FileInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CsvHeader&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewLine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewLine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Unload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;Quit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCommandLineArgs&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="cp"&gt;#if !UNITY_EDITOR
&lt;/span&gt;        &lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Quit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example launches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Builds\DS-Off\Game.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-bundleName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ds_test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-condition&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DS-Off&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-runIndex&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Builds\DS-On\Game.exe&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-bundleName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ds_test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-condition&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DS-On&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-runIndex&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;condition&lt;/code&gt; is only an aggregation label. In production, store OS, Graphics API, profiler state, cold/warm state, Build Profile, and diagnostic/release mode in separate columns.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Mount&lt;/code&gt;, &lt;code&gt;Assets&lt;/code&gt;, and &lt;code&gt;Total&lt;/code&gt; are wall-clock times until the coroutine resumes. They can include PlayerLoop scheduling, frame boundaries, and main-thread congestion; they are not pure storage timings. Use PIX, the Profiler, and &lt;code&gt;AsyncReadManagerMetrics&lt;/code&gt; for queue-level analysis. The first run can also include JIT, type initialization, and first-use paths.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LoadAllAssetsAsync&lt;/code&gt; is deliberately a stress test. Also measure the actual Addressables key, scene, dependency, or subset-loading path used by the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Binary Data with &lt;code&gt;AsyncReadManager&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AsyncReadManager.Read&lt;/code&gt; accepts a path and &lt;code&gt;ReadCommand&lt;/code&gt; values containing offset, size, and destination-buffer information. A &lt;code&gt;ReadHandle&lt;/code&gt; represents progress and completion.&lt;/p&gt;

&lt;p&gt;This is not a mechanical replacement for &lt;code&gt;File.ReadAllBytes&lt;/code&gt;. Production code must define unsafe-buffer ownership, allocator and release pairing, success/failure/cancellation cleanup, &lt;code&gt;ReadHandle.Dispose()&lt;/code&gt; timing, range validation, and the boundary between background parsing and main-thread application. A mistake can cause a crash, memory corruption, or a leak, so start from Unity's official sample or a tested I/O wrapper.&lt;/p&gt;

&lt;p&gt;Do not submit a read and immediately block the main thread on completion. Separate the pipeline and measure it as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ReadMs  : Data reaches the buffer
ParseMs : Bytes become structured data
ApplyMs : Parsed data reaches Unity-side state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For open-world data, read a small header and chunk table first, then prioritize nearby cells. Request granularity, priority, cancellation, and backpressure remain game-side responsibilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirm the Path with Metrics and PIX
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;AsyncReadManagerMetrics&lt;/code&gt; only in diagnostic builds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Unity.IO.LowLevel.Unsafe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cp"&gt;#if ENABLE_PROFILER
&lt;/span&gt;&lt;span class="n"&gt;AsyncReadManagerMetrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartCollectingMetrics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Run the workload.&lt;/span&gt;
&lt;span class="n"&gt;AsyncReadManagerMetrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StopCollectingMetrics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-enable-file-read-metrics&lt;/code&gt; starts collection at process launch. Remove instrumentation and repeat the final measurement with a release-like build.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://devblogs.microsoft.com/pix/pix-support-for-directstorage/" rel="noopener noreferrer"&gt;PIX Timing Capture&lt;/a&gt;, enable &lt;strong&gt;File IO &amp;gt; File accesses&lt;/strong&gt;. Compare the presence of the DirectStorage queue, Enqueue/Submit pattern, batch size, duration, I/O-waiting threads, and main-thread stalls between the two builds.&lt;/p&gt;

&lt;p&gt;Unity supports DirectStorage on Windows 10 and 11, but Windows 11 is the better-optimized environment. First prove activation through the setting, Build Profile, and PIX queue; then compare Windows version, NVMe or SATA storage, BitLocker, and filter drivers.&lt;/p&gt;

&lt;p&gt;On Windows 11, &lt;code&gt;fsutil bypassIo&lt;/code&gt; can report I/O-stack blockers. It is a supporting diagnostic, not proof that Unity used DirectStorage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;fsutil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bypassIo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\Builds\DS-On"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command accepts a volume, directory, or file path. Combine it with PIX and elapsed-time results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply Existing Loading-Optimization Experience
&lt;/h2&gt;

&lt;p&gt;DirectStorage rewards rather than replaces good loading architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove synchronous choke points such as large &lt;code&gt;File.ReadAllBytes&lt;/code&gt; calls and tasks completed immediately on the main thread.&lt;/li&gt;
&lt;li&gt;Chunk large files so important data can be prioritized and cancelled.&lt;/li&gt;
&lt;li&gt;Overlap work: read chunk &lt;code&gt;N+2&lt;/code&gt;, parse &lt;code&gt;N+1&lt;/code&gt;, and apply &lt;code&gt;N&lt;/code&gt; on the main thread.&lt;/li&gt;
&lt;li&gt;Revisit bundle boundaries and duplicated dependencies.&lt;/li&gt;
&lt;li&gt;Track native buffers, decompressed data, Unity objects, and GPU-upload memory as overlapping peaks.&lt;/li&gt;
&lt;li&gt;Judge success by time-to-interaction, median, p95, worst-frame duration, and minimum-spec memory—not the best run on a development PC.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Production Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Unity 6.5 Windows 64-bit player and exact patch recorded&lt;/li&gt;
&lt;li&gt;[ ] Every condition except DirectStorage fixed; Git commit recorded&lt;/li&gt;
&lt;li&gt;[ ] DirectStorage queue appears only in the enabled PIX capture&lt;/li&gt;
&lt;li&gt;[ ] Workload uses a supported asset type or &lt;code&gt;AsyncReadManager&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Stress test and actual product path both measured&lt;/li&gt;
&lt;li&gt;[ ] I/O, parsing, object creation, and activation separated&lt;/li&gt;
&lt;li&gt;[ ] LZ4 or uncompressed path validated before LZMA workflow testing&lt;/li&gt;
&lt;li&gt;[ ] Cold and warm runs separated; median and p95 retained&lt;/li&gt;
&lt;li&gt;[ ] OS, storage, BitLocker, filters, worst frame, and memory recorded&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Unity 6.5 extends DirectStorage beyond built-in Texture, Mesh, and DOTS data to qualifying &lt;code&gt;AsyncReadManager&lt;/code&gt; reads.&lt;/p&gt;

&lt;p&gt;The public 7.49-second to 6.10-second case is an 18.6% elapsed-time reduction, not a promise for every project. Unity's “up to 40%” figure applies to favorable eligible I/O; its effect on total loading depends on how much of the pipeline that I/O occupies.&lt;/p&gt;

&lt;p&gt;Build controlled off/on Windows players, confirm the enabled queue in PIX, separate storage from parsing and application, and decide from median, p95, worst-frame time, and memory on target hardware. DirectStorage is not a substitute for loading optimization. It is a lower-level path that lets good chunking, parallelism, and measurement pay off more effectively.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Unity Documentation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/windows-directstorage.html" rel="noopener noreferrer"&gt;Optimize performance using DirectStorage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/WhatsNewUnity65.html" rel="noopener noreferrer"&gt;New in Unity 6.5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://unity.com/releases/editor/whats-new/6000.5.4f1" rel="noopener noreferrer"&gt;Unity 6000.5.4f1 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://unity.com/releases/editor/whats-new/6000.5.0f1" rel="noopener noreferrer"&gt;Unity 6000.5.0f1 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://issuetracker.unity3d.com/issues/enabling-slash-disabling-direct-storage-on-windows-default-build-profile-is-broken" rel="noopener noreferrer"&gt;Issue Tracker: UUM-133978&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/PlayerSettings-enableDirectStorage.html" rel="noopener noreferrer"&gt;&lt;code&gt;PlayerSettings.enableDirectStorage&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Build.Profile.BuildProfile.GetActiveBuildProfile.html" rel="noopener noreferrer"&gt;&lt;code&gt;BuildProfile.GetActiveBuildProfile&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Unity.IO.LowLevel.Unsafe.AsyncReadManager.Read.html" rel="noopener noreferrer"&gt;&lt;code&gt;AsyncReadManager.Read&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Unity.IO.LowLevel.Unsafe.AsyncReadManagerMetrics.StartCollectingMetrics.html" rel="noopener noreferrer"&gt;&lt;code&gt;AsyncReadManagerMetrics.StartCollectingMetrics&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/assetbundles-optimizing.html" rel="noopener noreferrer"&gt;Optimizing AssetBundle memory usage&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Unity Discussions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://discussions.unity.com/t/direct-storage-in-unity-where-we-are-and-whats-next/1716802" rel="noopener noreferrer"&gt;Unity staff: Direct Storage in Unity—Where We Are and What's Next&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discussions.unity.com/t/unity-announces-directstorage-support-in-unity-6-4-beta-enabling-accelerated-asset-loading-on-windows/1703061?page=2" rel="noopener noreferrer"&gt;User benchmark discussion, page 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discussions.unity.com/t/unity-announces-directstorage-support-in-unity-6-4-beta-enabling-accelerated-asset-loading-on-windows/1703061?page=4" rel="noopener noreferrer"&gt;User benchmark discussion, page 4&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Microsoft Documentation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devblogs.microsoft.com/pix/pix-support-for-directstorage/" rel="noopener noreferrer"&gt;PIX support for DirectStorage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/windows-hardware/drivers/ifs/bypassio" rel="noopener noreferrer"&gt;BypassIO for filter drivers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>performance</category>
      <category>windows</category>
    </item>
    <item>
      <title>Unity 6.6 Finally Supports Dictionary Serialization: Do We Still Need Custom Implementations or Odin?</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Sun, 26 Jul 2026 00:25:38 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/unity-66-finally-supports-dictionary-serialization-do-we-still-need-custom-implementations-or-39mg</link>
      <guid>https://dev.to/gamedevtoollab/unity-66-finally-supports-dictionary-serialization-do-we-still-need-custom-implementations-or-39mg</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Unity projects constantly need mappings: an item ID to its definition, a status-effect type to its parameters, an input name to a Sprite, or a cell coordinate to its gameplay data. In ordinary C#, &lt;code&gt;Dictionary&amp;lt;TKey, TValue&amp;gt;&lt;/code&gt; is the natural choice.&lt;/p&gt;

&lt;p&gt;Until now, however, Unity could not directly serialize a regular Dictionary into a Scene, Prefab, or ScriptableObject, and the default Inspector could not display one. Teams usually worked around that limitation by converting keys and values into two Lists through &lt;code&gt;ISerializationCallbackReceiver&lt;/code&gt;, adopting an existing &lt;code&gt;SerializableDictionary&lt;/code&gt;, or bringing in Odin Inspector and Odin Serializer.&lt;/p&gt;

&lt;p&gt;Unity 6.6 finally adds built-in serialization for the standard &lt;code&gt;Dictionary&amp;lt;TKey, TValue&amp;gt;&lt;/code&gt;. The Inspector gets a dedicated editor with entry addition and removal, sorting, duplicate-key detection, and adjustable column widths.&lt;/p&gt;

&lt;p&gt;That does &lt;strong&gt;not&lt;/strong&gt; mean every custom serializer or every use of Odin immediately becomes obsolete. This article looks at the supported range, Inspector behavior, Prefab pitfalls, and migration concerns so that you can decide where the new built-in support is enough—and where it is not.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; This article is based on the Unity 6.6 Beta documentation available on July 15, 2026, specifically &lt;code&gt;6000.6.0b3&lt;/code&gt;. Attribute names, behavior, and limitations may change before the final release. Before adopting the feature in production, check the release notes and manual for the exact Unity version you intend to ship.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The conclusion first
&lt;/h2&gt;

&lt;p&gt;When all of the following are true, there is now far less reason to create a new &lt;code&gt;SerializableDictionary&lt;/code&gt; implementation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unity 6.6 or later can be your minimum supported Editor version.&lt;/li&gt;
&lt;li&gt;The key and value types fit Unity's supported serialization rules.&lt;/li&gt;
&lt;li&gt;The standard Inspector provides enough editing functionality.&lt;/li&gt;
&lt;li&gt;You can accept the current Prefab Override behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, a &lt;strong&gt;plain Dictionary&lt;/strong&gt; means the exact, non-derived &lt;code&gt;Dictionary&amp;lt;TKey, TValue&amp;gt;&lt;/code&gt; type whose key and value are both supported by Unity serialization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections.Generic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CreateAssetMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;menuName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Example/Item Price Table"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ItemPriceTable&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ScriptableObject&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;TryGetPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example can be created from &lt;code&gt;Create &amp;gt; Example &amp;gt; Item Price Table&lt;/code&gt; in the Project window. The &lt;code&gt;prices&lt;/code&gt; field is stored in the ScriptableObject asset and can be edited in the Inspector.&lt;/p&gt;

&lt;p&gt;The standard feature does not replace every existing solution:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Default direction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Support Unity 6.5 or earlier&lt;/td&gt;
&lt;td&gt;Keep the existing custom implementation or Odin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serialize &lt;code&gt;SortedDictionary&lt;/code&gt;, &lt;code&gt;HashSet&lt;/code&gt;, or a Dictionary-derived type&lt;/td&gt;
&lt;td&gt;Use custom serialization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Store interfaces, abstract types, or polymorphic managed references&lt;/td&gt;
&lt;td&gt;Consider Odin or another dedicated mechanism&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provide search, bulk editing, or advanced validation UI&lt;/td&gt;
&lt;td&gt;A Custom Editor or Odin is still useful&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migrate a large amount of existing custom or Odin data&lt;/td&gt;
&lt;td&gt;Plan an explicit migration process&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most accurate interpretation is not “Unity solved every serialization problem.” It is that the extremely common need to &lt;strong&gt;serialize an ordinary Dictionary without a custom wrapper&lt;/strong&gt; has finally become a built-in feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Dictionary serialization used to be painful
&lt;/h2&gt;

&lt;p&gt;Through Unity 6.5, the official serialization rules did not support Dictionary. A common workaround copied entries into &lt;code&gt;List&amp;lt;TKey&amp;gt;&lt;/code&gt; and &lt;code&gt;List&amp;lt;TValue&amp;gt;&lt;/code&gt; before serialization, then paired elements with the same index to reconstruct the Dictionary after loading.&lt;/p&gt;

&lt;p&gt;The basic idea is simple. Production behavior is not. A team still has to define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happens when the key and value counts differ.&lt;/li&gt;
&lt;li&gt;How null and duplicate keys are handled.&lt;/li&gt;
&lt;li&gt;What work is safe inside serialization callbacks.&lt;/li&gt;
&lt;li&gt;How Undo and Prefab Overrides behave in a custom Inspector.&lt;/li&gt;
&lt;li&gt;Whether each concrete type needs a derived class or Property Drawer.&lt;/li&gt;
&lt;li&gt;How old serialized data remains compatible after the implementation changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A short serializer may be enough to “make it save.” Turning it into a tool that an entire team can edit safely is much more expensive. That is also why Odin has remained popular: it can solve both serialization and Inspector presentation instead of leaving every project to maintain both layers.&lt;/p&gt;

&lt;p&gt;The important part of Unity 6.6 is therefore not merely that Dictionary data can be stored. Unity itself now owns the Inspector UI, warnings, Prefab Override integration, and serialization-rule diagnostics around the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal setup—and &lt;code&gt;[SerializeField]&lt;/code&gt; is required even for public fields
&lt;/h2&gt;

&lt;p&gt;The basic form is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections.Generic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Inventory&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;itemCounts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same feature is available in &lt;code&gt;ScriptableObject&lt;/code&gt; and inside nested classes or structs marked with &lt;code&gt;[Serializable]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The easiest rule to miss is that Dictionary requires explicit opt-in. Unlike many traditionally supported Unity field types, making the field public is not enough.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Demonstration only: public, but not serialized.&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;notSerialized&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Demonstration only: public and explicitly serialized.&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;serialized&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;notSerialized&lt;/code&gt; remains unsaved even though it is public. Any Dictionary that should be serialized needs &lt;code&gt;[SerializeField]&lt;/code&gt;, whether the field is private or public.&lt;/p&gt;

&lt;p&gt;This avoids turning every public Dictionary in an existing project into serialized data merely because the Editor was upgraded. It makes serialization intentional rather than implicit.&lt;/p&gt;

&lt;p&gt;In production code, &lt;code&gt;private&lt;/code&gt; plus &lt;code&gt;[SerializeField]&lt;/code&gt; is usually easier to control than a freely replaceable public field. Expose only the operations or read-only access that other code actually needs.&lt;/p&gt;

&lt;p&gt;The serialization rules analyzer reports a public Dictionary without &lt;code&gt;[SerializeField]&lt;/code&gt; as &lt;code&gt;UAC1015&lt;/code&gt;. If a Dictionary is deliberately a runtime-only cache, consider marking that intent with &lt;code&gt;[NonSerialized]&lt;/code&gt; rather than leaving its role ambiguous.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;[SerializeReference]&lt;/code&gt; cannot be applied to the Dictionary field
&lt;/h3&gt;

&lt;p&gt;Dictionary entries are stored inline as a sequence of key-value entries, so &lt;code&gt;[SerializeReference]&lt;/code&gt; cannot be applied to the Dictionary field itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeReference&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;// Invalid&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IEffect&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;effects&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example is unsupported for a second reason: the value type is an interface. Unity 6.6's Dictionary feature does not extend the polymorphic managed-reference behavior of &lt;code&gt;[SerializeReference]&lt;/code&gt; into Dictionary fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  The declared type must be exactly &lt;code&gt;Dictionary&amp;lt;TKey, TValue&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The manual requires the field's declared type to be the exact &lt;code&gt;Dictionary&amp;lt;TKey, TValue&amp;gt;&lt;/code&gt; type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections.Generic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Serializable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ItemPriceDictionary&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LegacyItemDatabase&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ScriptableObject&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;ItemPriceDictionary&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Not handled by the new built-in backend&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an existing project uses derived types such as &lt;code&gt;StringIntDictionary&lt;/code&gt;, upgrading to Unity 6.6 does not silently convert them to the new built-in representation. You need to change the declared type and migrate the stored data explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the built-in Inspector can and cannot do
&lt;/h2&gt;

&lt;p&gt;The standard Dictionary editor supports at least the following operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add and remove entries with &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;-&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Resize the key and value columns.&lt;/li&gt;
&lt;li&gt;Sort the displayed rows by key in ascending or descending order.&lt;/li&gt;
&lt;li&gt;Warn about duplicate and null keys.&lt;/li&gt;
&lt;li&gt;Switch the display layout.&lt;/li&gt;
&lt;li&gt;Show Prefab Overrides at the key or value level.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That covers a great deal of ordinary project configuration, but several details matter in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sorting changes the display, not the stored order
&lt;/h3&gt;

&lt;p&gt;Clicking the key column changes only the order shown in the Inspector. The serialized entry order remains insertion order.&lt;/p&gt;

&lt;p&gt;This distinction matters for YAML diffs and Prefab Overrides. A Dictionary may look alphabetically sorted in the Inspector while the serialized file still follows a different order. Game logic also should not assign meaning to the Dictionary's enumeration order.&lt;/p&gt;

&lt;p&gt;When priority or presentation order is part of the specification, store it explicitly in the value or use a List as the authoritative structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Duplicate keys can remain temporarily in serialized data
&lt;/h3&gt;

&lt;p&gt;The Inspector does not immediately delete a duplicate row while you are editing. It keeps the row and shows a warning so that you can correct the data without losing the value.&lt;/p&gt;

&lt;p&gt;According to the Unity 6.6 Beta documentation, loading serialized data that still contains duplicates produces a warning, and only the first entry in serialized order is added to the runtime Dictionary. Verify that behavior again in the final release and in the exact patch version used by your project.&lt;/p&gt;

&lt;p&gt;As a result, the number of rows visible in serialized data may differ from &lt;code&gt;Dictionary.Count&lt;/code&gt; at runtime. Which value survives depends on serialized insertion order, not the current sort order in the Inspector.&lt;/p&gt;

&lt;p&gt;Do not treat the warning as harmless editor noise. Duplicate-key checks should be part of your content-validation process.&lt;/p&gt;

&lt;p&gt;A Custom Inspector can retrieve duplicate positions through &lt;code&gt;SerializedProperty.GetDictionaryDuplicateEntryIndices&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Null keys are preserved as placeholders, then omitted at runtime
&lt;/h3&gt;

&lt;p&gt;When a &lt;code&gt;UnityEngine.Object&lt;/code&gt; reference is used as a key, an unassigned field or missing reference can create a null-key row. The Inspector preserves that row as a placeholder so that the value is not immediately lost during editing.&lt;/p&gt;

&lt;p&gt;A runtime Dictionary still cannot contain a null key. Unity warns during loading and leaves that entry out of the reconstructed Dictionary.&lt;/p&gt;

&lt;p&gt;In other words, “visible in the Inspector” does not necessarily mean “present in the runtime Dictionary.”&lt;/p&gt;

&lt;h3&gt;
  
  
  There is no Multi-Object Editing, search, or filtering
&lt;/h3&gt;

&lt;p&gt;In the Unity 6.6 Beta, Dictionary fields do not support Multi-Object Editing. Selecting multiple GameObjects or assets displays a Help Box instead of the Dictionary editor.&lt;/p&gt;

&lt;p&gt;The standard Inspector also has no entry search or filtering. That is perfectly reasonable for a few dozen entries. It becomes a workflow problem when designers or developers must edit hundreds or thousands of records directly.&lt;/p&gt;

&lt;p&gt;At that scale, a Custom Editor or external authoring pipeline is still the better tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supported key and value types
&lt;/h2&gt;

&lt;p&gt;The following are representative supported types rather than an exhaustive list. Run the exact types used by your project through the serialization rules analyzer for the Unity version you adopt.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Primitive and common built-in types such as &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;float&lt;/code&gt;, &lt;code&gt;double&lt;/code&gt;, &lt;code&gt;bool&lt;/code&gt;, and &lt;code&gt;string&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enums whose underlying type is 32 bits or smaller.&lt;/li&gt;
&lt;li&gt;Unity built-in serializable types such as &lt;code&gt;Vector2&lt;/code&gt;, &lt;code&gt;Vector3&lt;/code&gt;, and &lt;code&gt;Color&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Custom classes and structs marked with &lt;code&gt;[Serializable]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;References to types derived from &lt;code&gt;UnityEngine.Object&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ordinary interfaces, abstract types, and custom types without &lt;code&gt;[Serializable]&lt;/code&gt; are not supported. &lt;code&gt;UnityEngine.Object&lt;/code&gt; inheritance is different because those values are stored as Unity object references.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lists and arrays can be values, but not keys
&lt;/h3&gt;

&lt;p&gt;These fields are valid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stageEnemies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scoreTables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Collection-like &lt;code&gt;IEnumerable&lt;/code&gt; types such as List and array cannot be used as keys. &lt;code&gt;string&lt;/code&gt; is the exception.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Unsupported&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;invalid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A mutable collection is a dangerous key even outside Unity's restrictions. Changing its contents can change equality or hash behavior after it has already been inserted.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Dictionary cannot be placed directly inside a List or array
&lt;/h3&gt;

&lt;p&gt;The following forms are unsupported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;histories&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Unsupported&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;[]&lt;/span&gt; &lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Unsupported&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrap the Dictionary in a serializable class or struct instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Serializable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ItemCountTable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ItemCountTable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;snapshots&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The official manual also shows a Dictionary used as the value of another Dictionary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SkillLevel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;skillsPerTier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That combination can feel surprising: &lt;code&gt;List&amp;lt;Dictionary&amp;lt;...&amp;gt;&amp;gt;&lt;/code&gt; is unsupported, while a nested Dictionary value is documented. Do not infer the nesting rules from intuition. Run the concrete type through the analyzer and verify it by saving and reloading the Scene or asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom key types still need a correct equality design
&lt;/h2&gt;

&lt;p&gt;When a custom class or struct is used as a key, implement &lt;code&gt;Equals&lt;/code&gt; and &lt;code&gt;GetHashCode&lt;/code&gt; appropriately. Without an override, two class instances with identical serialized fields are still different keys because reference equality is used.&lt;/p&gt;

&lt;p&gt;A struct can work with its default equality behavior, but Unity's documentation recommends implementing &lt;code&gt;IEquatable&amp;lt;T&amp;gt;&lt;/code&gt; for value-type keys rather than relying on reflection-based default comparison.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Serializable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;GridCellKey&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEquatable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GridCellKey&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GridCellKey&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;GridCellKey&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;GetHashCode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;unchecked&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;397&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;^&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For class keys in particular, do not modify fields that participate in equality after inserting the object into a Dictionary. If the hash code changes, the key can become effectively unreachable even though the entry still exists internally.&lt;/p&gt;

&lt;p&gt;Struct keys are copied on insertion, but the clearest way to change a key is still to remove the old entry and add a new one. Keep mutable state in the value and treat keys as effectively immutable.&lt;/p&gt;

&lt;p&gt;If a custom &lt;code&gt;Equals&lt;/code&gt; or &lt;code&gt;GetHashCode&lt;/code&gt; implementation throws while Unity reconstructs the Dictionary, Unity skips affected entries and logs a warning. Built-in serialization cannot protect a project from an incorrect equality implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serializable does not always mean Dictionary is the right structure
&lt;/h2&gt;

&lt;p&gt;“Unity can now save it” and “Dictionary best represents the data” are separate questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The order itself has meaning
&lt;/h3&gt;

&lt;p&gt;Dialogue sequence, attack priority, tutorial progression, and similar data belong naturally in a List. Dictionary exists for key-based lookup, and its Inspector sorting is display-only.&lt;/p&gt;

&lt;p&gt;When order is part of the specification, keep the List as the source of truth and build a lookup Dictionary during initialization when necessary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ItemDefinition&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ItemDefinition&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;itemById&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;itemById&lt;/code&gt; is a derived cache. It should not be serialized; rebuild it in &lt;code&gt;Awake&lt;/code&gt; or another initialization step. Not every Dictionary needs &lt;code&gt;[SerializeField]&lt;/code&gt; simply because the option now exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple rows with the same key are legitimate data
&lt;/h3&gt;

&lt;p&gt;An enemy may have several drop candidates. Multiple events may occur at the same timestamp. In those cases, repeated keys are not invalid input—they are the model.&lt;/p&gt;

&lt;p&gt;Do not add artificial sequence numbers merely to force uniqueness. Represent the relationship directly with something like &lt;code&gt;Dictionary&amp;lt;EnemyId, List&amp;lt;DropEntry&amp;gt;&amp;gt;&lt;/code&gt; or with an ordinary List.&lt;/p&gt;

&lt;h3&gt;
  
  
  An external format is the source of truth
&lt;/h3&gt;

&lt;p&gt;A server API, CSV file, spreadsheet, or existing save-data schema may own the real contract. The fact that Unity can edit a Dictionary in the Inspector is not a reason to replace that external schema with Unity's internal representation.&lt;/p&gt;

&lt;p&gt;Keeping external DTOs separate from Unity authoring data, then converting at a boundary, often makes compatibility much easier to manage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Humans must edit a very large dataset
&lt;/h3&gt;

&lt;p&gt;The standard Inspector has no search or filtering. For large datasets, input validation, diff review, referential integrity, and bulk updates matter more than whether the final generated object happens to be a serialized Dictionary.&lt;/p&gt;

&lt;p&gt;The built-in Dictionary may be a useful storage target. It is not a replacement for the complete data-authoring workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customizing the Inspector with &lt;code&gt;DictionaryDisplay&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;[DictionaryDisplay]&lt;/code&gt; lets you define initial column labels, column width, and layout.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections.Generic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Serializable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ItemDefinition&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Sprite&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CreateAssetMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;menuName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Example/Item Database"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ItemDatabase&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ScriptableObject&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DictionaryDisplay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;keyLabel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Item ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;valueLabel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Definition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;keyColumnFraction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.35f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DictionaryLayout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OneColumnWithValueFoldout&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ItemDefinition&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DictionaryLayout&lt;/code&gt; provides three layouts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Display&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TwoColumns&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shows the key and value side by side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OneColumnWithValueVisible&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Always shows the value below the key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OneColumnWithValueFoldout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shows the value below the key in a Foldout&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The attribute defines the initial presentation. If a user changes the column width or layout in the Inspector, Unity stores that preference per property path and the user setting takes precedence afterward.&lt;/p&gt;

&lt;p&gt;Choose &lt;code&gt;Reset to Defaults&lt;/code&gt; from the column-header menu to return to the attribute's values.&lt;/p&gt;

&lt;p&gt;For a shared configuration applied to the same closed Dictionary type, Unity also provides the assembly-level &lt;code&gt;[DictionaryDisplayForType]&lt;/code&gt; attribute.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections.Generic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;assembly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;DictionaryDisplayForType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SkillLevel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;),&lt;/span&gt;
    &lt;span class="n"&gt;keyLabel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Skill"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;valueLabel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Level"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DictionaryLayout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OneColumnWithValueVisible&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Serializable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;SkillLevel&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;bonus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful for nested Dictionaries where no directly annotated field exists. The target must be an exact closed Dictionary type, and the assembly declaring the attribute must itself define the key type, value type, or one of the nested types involved.&lt;/p&gt;

&lt;p&gt;The restriction prevents one assembly from globally changing the display of a Dictionary made entirely from types it does not own. Consequently, an assembly cannot apply a global setting to a combination such as &lt;code&gt;Dictionary&amp;lt;string, int&amp;gt;&lt;/code&gt; when both types come only from the .NET Base Class Library.&lt;/p&gt;

&lt;p&gt;Because this attribute affects an entire assembly, reusable packages should also consider how broad that display policy becomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefab Overrides are tracked by serialized position, not by key
&lt;/h2&gt;

&lt;p&gt;Unity 6.6 lets an individual Dictionary key or value be overridden in a Prefab instance. The override is not tracked by the logical key, however. It is recorded against the serialized entry position, effectively behaving like a List or array index.&lt;/p&gt;

&lt;p&gt;The Inspector's displayed sort order can differ from the stored insertion order, so the row visible on screen does not necessarily correspond to the serialized position used by the override.&lt;/p&gt;

&lt;p&gt;If entries are added or removed in the Prefab asset and serialized positions shift, an instance override may end up applying to a different key than the one originally intended. The official manual therefore warns developers to check instance overrides after editing the asset.&lt;/p&gt;

&lt;p&gt;Practical precautions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Move frequently edited shared tables into ScriptableObject assets.&lt;/li&gt;
&lt;li&gt;Check Prefab variants and instances after adding or removing entries in the Prefab asset.&lt;/li&gt;
&lt;li&gt;Review YAML diffs and the Overrides list rather than trusting only the visible Inspector order.&lt;/li&gt;
&lt;li&gt;Never assume that an override follows an entry merely because its key text remains the same.&lt;/li&gt;
&lt;li&gt;Avoid mixing Dictionary migration and unrelated Prefab changes in the same commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is arguably the most important trap in the first version of the feature: the data structure is key-based, but Prefab Override tracking is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the serialization rules analyzer to catch unsupported shapes
&lt;/h2&gt;

&lt;p&gt;Unity 6.6's analyzer reports several common Dictionary mistakes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Diagnostic&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UAC1009&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A Dictionary is placed directly inside an unsupported collection shape&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UAC1012&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An interface or abstract type is used as the key or value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UAC1013&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An &lt;code&gt;IEnumerable&lt;/code&gt; type is used as the key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UAC1014&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;[SerializeReference]&lt;/code&gt; is applied to a Dictionary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UAC1015&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A public Dictionary does not have &lt;code&gt;[SerializeField]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UAC1016&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The key or value type is not serializable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This makes it easier to catch the old failure mode where code compiles but the data is silently not persisted as expected.&lt;/p&gt;

&lt;p&gt;Monitor these analyzer warnings in CI as well, and avoid suppressing Dictionary-related warnings without a deliberate reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do custom &lt;code&gt;SerializableDictionary&lt;/code&gt; implementations become unnecessary?
&lt;/h2&gt;

&lt;p&gt;Unity's custom serialization documentation now recommends using the built-in serializer for ordinary Dictionaries and reserving callback-based conversion for collection types Unity still cannot handle directly.&lt;/p&gt;

&lt;p&gt;For a new implementation, start with the built-in Dictionary when all of these conditions hold:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The minimum supported version is Unity 6.6 or later.&lt;/li&gt;
&lt;li&gt;A plain &lt;code&gt;Dictionary&amp;lt;TKey, TValue&amp;gt;&lt;/code&gt; is sufficient.&lt;/li&gt;
&lt;li&gt;Both key and value are within the supported range.&lt;/li&gt;
&lt;li&gt;The standard Inspector supports the editing workflow.&lt;/li&gt;
&lt;li&gt;The Prefab Override limitations are acceptable.&lt;/li&gt;
&lt;li&gt;There is no external serialized format that must remain stable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Custom implementations still have a role when you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SortedDictionary&lt;/code&gt;, &lt;code&gt;HashSet&lt;/code&gt;, or a Dictionary-derived type.&lt;/li&gt;
&lt;li&gt;Normalization, compression, encryption, or ID conversion while saving.&lt;/li&gt;
&lt;li&gt;A fixed schema shared with tools outside Unity.&lt;/li&gt;
&lt;li&gt;Backward compatibility with older Unity versions.&lt;/li&gt;
&lt;li&gt;Custom duplicate merging, change notifications, or automatic key generation.&lt;/li&gt;
&lt;li&gt;Import from CSV or spreadsheets.&lt;/li&gt;
&lt;li&gt;Search, bulk editing, or specialized validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When an existing &lt;code&gt;SerializableDictionary&lt;/code&gt; owns responsibilities beyond storage, replacing only its type mechanically is unsafe.&lt;/p&gt;

&lt;p&gt;It is also too early to claim that the built-in implementation always loads several times faster or always creates smaller files. The largest guaranteed benefit is maintenance: less conversion code, fewer Drawers, fewer compatibility paths, and a smaller test surface.&lt;/p&gt;

&lt;p&gt;Measure performance with your actual data when that difference matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Odin become unnecessary?
&lt;/h2&gt;

&lt;p&gt;If a project uses Odin only to serialize and display ordinary Dictionaries, Unity 6.6 may let you reduce that dependency. Moving plain Dictionaries back to Unity's built-in serializer can reduce the number of serialization paths and package-specific behaviors the project must maintain.&lt;/p&gt;

&lt;p&gt;Sirenix's own best-practice guidance also recommends leaving data to Unity's serializer when Unity can handle it and limiting Odin serialization to the places that require it.&lt;/p&gt;

&lt;p&gt;Odin's value is much broader than Dictionary support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A wider range of types than Unity's built-in serializer supports.&lt;/li&gt;
&lt;li&gt;Interfaces and polymorphic data structures.&lt;/li&gt;
&lt;li&gt;Inspector grouping, buttons, and conditional display.&lt;/li&gt;
&lt;li&gt;Validation, custom Drawers, and editor tooling.&lt;/li&gt;
&lt;li&gt;Compatibility with existing Odin Serializer data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a project is deeply built around Odin attributes and editor workflows, removing the package only because Unity gained Dictionary support may provide little benefit. A practical hybrid is to use Unity's standard serializer for new, simple Dictionaries and keep Odin for complex types and advanced Inspector UI.&lt;/p&gt;

&lt;p&gt;The dangerous migration order is to upgrade Unity, change the field types, and remove Odin before reading the old data. Once the old serialization backend is gone, those values may no longer be accessible.&lt;/p&gt;

&lt;p&gt;Keep Odin operational while reading the old fields, copy the data into the new standard Dictionaries, save and reload every affected asset, and remove the dependency only after verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating existing data safely
&lt;/h2&gt;

&lt;p&gt;Do not assume that replacing a custom type or Odin-managed field with &lt;code&gt;Dictionary&amp;lt;TKey, TValue&amp;gt;&lt;/code&gt; automatically migrates existing serialized data. The stored representation and serialization backend can differ, so the project needs an explicit migration step.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[FormerlySerializedAs]&lt;/code&gt; preserves an old field name. It is not a universal converter between unrelated types or serialization systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recommended migration sequence
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a dedicated migration branch and a backup.&lt;/li&gt;
&lt;li&gt;Add the new standard Dictionary under a different field name while keeping the old field.&lt;/li&gt;
&lt;li&gt;Copy the old data into the new field with Editor-only code.&lt;/li&gt;
&lt;li&gt;Detect duplicates, null keys, missing references, and count differences.&lt;/li&gt;
&lt;li&gt;Save all affected ScriptableObjects, Prefabs, and Scenes.&lt;/li&gt;
&lt;li&gt;Reload scripts, restart the Editor, and test a Player build.&lt;/li&gt;
&lt;li&gt;Compare key counts, keys, and values between the old and new formats mechanically.&lt;/li&gt;
&lt;li&gt;Remove the old field, custom Drawer, or Odin dependency only after the comparison passes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;During migration, the fields may temporarily look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HideInInspector&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;LegacyStringIntDictionary&lt;/span&gt; &lt;span class="n"&gt;legacyPrices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following is a conceptual migration example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#if UNITY_EDITOR
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ContextMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Migration/Copy legacy prices"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CopyLegacyPrices&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;migrated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;hasError&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pair&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;legacyPrices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Null key was found."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;hasError&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;migrated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ContainsKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Duplicate key: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;hasError&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;migrated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Migration was aborted."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;UnityEditor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Undo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RecordObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Migrate item prices"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;prices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;migrated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;UnityEditor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EditorUtility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetDirty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;LegacyStringIntDictionary&lt;/code&gt; and its enumeration API with the types used by your own project.&lt;/p&gt;

&lt;p&gt;A Context Menu may be enough for a handful of assets. Large migrations should use a dedicated tool based on APIs such as &lt;code&gt;AssetDatabase&lt;/code&gt;, &lt;code&gt;PrefabUtility.LoadPrefabContents&lt;/code&gt;, and explicit Scene opening and closing so that no asset is skipped.&lt;/p&gt;

&lt;p&gt;Put a large migration tool in an &lt;code&gt;Editor&lt;/code&gt; folder or an Editor-only assembly definition so that &lt;code&gt;UnityEditor&lt;/code&gt; references cannot leak into a Player assembly. The sample fully qualifies &lt;code&gt;UnityEditor.Undo&lt;/code&gt; and &lt;code&gt;UnityEditor.EditorUtility&lt;/code&gt; and wraps its body in &lt;code&gt;#if UNITY_EDITOR&lt;/code&gt;, but separating the whole tool into an Editor assembly is safer.&lt;/p&gt;

&lt;p&gt;Seeing correct values in memory immediately after the copy is not enough. Restart the Editor, reopen the Scenes and Prefabs, and confirm that a Player build reconstructs the same values.&lt;/p&gt;

&lt;p&gt;For an Odin migration, keeping Odin installed until the old fields have been read successfully is especially important.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not assume &lt;code&gt;JsonUtility&lt;/code&gt; and network JSON are the same feature
&lt;/h2&gt;

&lt;p&gt;Unity 6.6's Dictionary support is primarily about Unity serialization for Scenes, Prefabs, ScriptableObjects, and similar Unity data.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;JsonUtility&lt;/code&gt; Scripting API says that it processes the same supported types as Unity's standard serializer, which can be read as implying Dictionary support. At the same time, the Unity 6.6 JSON Serialization manual available on July 15, 2026 still contains the older statement that &lt;code&gt;Dictionary&amp;lt;&amp;gt;&lt;/code&gt; is unsupported.&lt;/p&gt;

&lt;p&gt;Because those official pages are inconsistent, this article does not treat &lt;code&gt;JsonUtility&lt;/code&gt; Dictionary support as a settled guarantee.&lt;/p&gt;

&lt;p&gt;Even when a Beta build appears to work, test the exact target version for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A complete &lt;code&gt;ToJson&lt;/code&gt; and &lt;code&gt;FromJson&lt;/code&gt; round trip, including the actual JSON shape produced.&lt;/li&gt;
&lt;li&gt;Compatibility with external API schemas and existing save data.&lt;/li&gt;
&lt;li&gt;Behavior in an IL2CPP Player build.&lt;/li&gt;
&lt;li&gt;Null keys, duplicates, and custom key types.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;“Unity can persist this internally” and “this is an appropriate long-term save or network contract” are different claims.&lt;/p&gt;

&lt;p&gt;For external formats, choose the tool that fits the contract: Newtonsoft.Json, MessagePack, dedicated DTOs, or another deliberate schema may still be the better choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New Unity 6.6+ project with simple ID lookup data&lt;/td&gt;
&lt;td&gt;Start with the built-in Dictionary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active Unity 6.0–6.5 production project&lt;/td&gt;
&lt;td&gt;Do not upgrade to a Beta only for Dictionary support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hundreds or thousands of static game-data records&lt;/td&gt;
&lt;td&gt;The Dictionary may be a storage target, but use an external tool or dedicated Editor for authoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asset Store package or shared package that supports older Unity versions&lt;/td&gt;
&lt;td&gt;Keeping the existing implementation can be reasonable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project already uses Odin extensively&lt;/td&gt;
&lt;td&gt;Move only new, simple Dictionaries first and migrate incrementally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefab instances frequently override entries&lt;/td&gt;
&lt;td&gt;Understand position-based overrides and consider moving the table to a ScriptableObject&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Dictionary serialization does not provide type-safe code generation, referential-integrity checks, localization workflows, server sharing, or live data-update infrastructure. It fills a major gap in Unity serialization; it is not a complete game-data platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adoption checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Record whether the target Unity version is a Beta or a final release.&lt;/li&gt;
&lt;li&gt;[ ] Add &lt;code&gt;[SerializeField]&lt;/code&gt; to every Dictionary that should be persisted.&lt;/li&gt;
&lt;li&gt;[ ] Confirm that the declared type is exactly &lt;code&gt;Dictionary&amp;lt;TKey, TValue&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Confirm that both key and value are supported types.&lt;/li&gt;
&lt;li&gt;[ ] Implement &lt;code&gt;Equals&lt;/code&gt; and &lt;code&gt;GetHashCode&lt;/code&gt; for custom keys.&lt;/li&gt;
&lt;li&gt;[ ] Remove all duplicate and null keys.&lt;/li&gt;
&lt;li&gt;[ ] Remember that Inspector sorting and serialized order are different.&lt;/li&gt;
&lt;li&gt;[ ] Recheck variants and instances after changing a Prefab asset.&lt;/li&gt;
&lt;li&gt;[ ] Confirm that the lack of Multi-Object Editing and search is acceptable, or provide an alternative.&lt;/li&gt;
&lt;li&gt;[ ] Test Scene reload, Domain Reload, and Editor restart.&lt;/li&gt;
&lt;li&gt;[ ] Test a Player build on every relevant target platform.&lt;/li&gt;
&lt;li&gt;[ ] Compare migrated counts and contents mechanically.&lt;/li&gt;
&lt;li&gt;[ ] Round-trip test &lt;code&gt;JsonUtility&lt;/code&gt; in the exact target version when you use it.&lt;/li&gt;
&lt;li&gt;[ ] Do not remove old fields or Odin before migration verification passes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Unity 6.6 allows a standard &lt;code&gt;Dictionary&amp;lt;TKey, TValue&amp;gt;&lt;/code&gt; marked with &lt;code&gt;[SerializeField]&lt;/code&gt; to be stored in Unity assets and edited through the Inspector. Built-in sorting, duplicate and null warnings, and configurable layouts greatly reduce the need to build a serialization backend and Drawer merely because a project needs key-based lookup data.&lt;/p&gt;

&lt;p&gt;Important boundaries remain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[SerializeField]&lt;/code&gt; is required even on public Dictionary fields.&lt;/li&gt;
&lt;li&gt;Only the exact &lt;code&gt;Dictionary&amp;lt;TKey, TValue&amp;gt;&lt;/code&gt; declared type uses the new backend.&lt;/li&gt;
&lt;li&gt;Interfaces and abstract types are generally unsupported.&lt;/li&gt;
&lt;li&gt;A Dictionary cannot be placed directly inside a List or array.&lt;/li&gt;
&lt;li&gt;Multi-Object Editing, search, and filtering are unavailable in the Beta.&lt;/li&gt;
&lt;li&gt;Prefab Overrides follow serialized positions rather than logical keys.&lt;/li&gt;
&lt;li&gt;Existing custom and Odin formats require explicit migration.&lt;/li&gt;
&lt;li&gt;The Unity 6.6 Beta documentation is inconsistent about &lt;code&gt;JsonUtility&lt;/code&gt; support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, do custom implementations and Odin become unnecessary?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For the sole purpose of saving a plain Dictionary and editing it in the standard Inspector, they will be unnecessary in many Unity 6.6+ projects.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For unsupported types, advanced Inspector workflows, older Unity versions, custom external formats, or compatibility with existing data, they remain necessary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In new projects, the built-in Dictionary should become the first option. In existing projects, do not delete a working serialization system merely because the Editor gained a built-in alternative. Inventory the old system's responsibilities, migrate explicitly, reload everything, and verify the result before reducing dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://unity.com/releases/editor/beta" rel="noopener noreferrer"&gt;Unity Beta Program&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.6/Documentation/Manual/script-serialization-dictionaries.html" rel="noopener noreferrer"&gt;Unity 6.6 Manual: Dictionary serialization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.6/Documentation/Manual/script-serialization-analyzer.html" rel="noopener noreferrer"&gt;Unity 6.6 Manual: Serialization rules analyzer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.6/Documentation/Manual/script-serialization-custom-serialization.html" rel="noopener noreferrer"&gt;Unity 6.6 Manual: Custom serialization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.6/Documentation/ScriptReference/DictionaryDisplayAttribute.html" rel="noopener noreferrer"&gt;Unity 6.6 Scripting API: DictionaryDisplayAttribute&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.6/Documentation/ScriptReference/DictionaryDisplayForTypeAttribute.html" rel="noopener noreferrer"&gt;Unity 6.6 Scripting API: DictionaryDisplayForTypeAttribute&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.6/Documentation/ScriptReference/DictionaryLayout.html" rel="noopener noreferrer"&gt;Unity 6.6 Scripting API: DictionaryLayout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.6/Documentation/ScriptReference/JsonUtility.ToJson.html" rel="noopener noreferrer"&gt;Unity 6.6 Scripting API: JsonUtility.ToJson&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.6/Documentation/ScriptReference/JsonUtility.FromJson.html" rel="noopener noreferrer"&gt;Unity 6.6 Scripting API: JsonUtility.FromJson&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.6/Documentation/Manual/json-serialization.html" rel="noopener noreferrer"&gt;Unity 6.6 Manual: JSON Serialization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/script-serialization-rules.html" rel="noopener noreferrer"&gt;Unity 6.5 Manual: Serialization rules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://odininspector.com/tutorials/serialize-anything/serializing-dictionaries" rel="noopener noreferrer"&gt;Odin Inspector: Serializing Dictionaries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://odininspector.com/tutorials/serialize-anything/features-and-limitations" rel="noopener noreferrer"&gt;Odin Serializer: Features and Limitations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://odininspector.com/tutorials/serialize-anything/best-practices" rel="noopener noreferrer"&gt;Odin Serializer: Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>serialization</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Unity's Official MCP with Codex: Safely Evaluating Console Fixes, Scene Edits, and Prefab Creation</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Tue, 21 Jul 2026 06:47:09 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/unitys-official-mcp-with-codex-safely-evaluating-console-fixes-scene-edits-and-prefab-creation-4hof</link>
      <guid>https://dev.to/gamedevtoollab/unitys-official-mcp-with-codex-safely-evaluating-console-fixes-scene-edits-and-prefab-creation-4hof</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Having an AI write C# and then reviewing the Git diff before returning to Unity is no longer unusual.&lt;/p&gt;

&lt;p&gt;Unity work, however, rarely ends with a source-code change. A task may also require editing the Hierarchy, configuring Components, saving a Prefab, switching or saving a Scene, waiting for compilation, and checking the Console after a Domain Reload. An AI that can only inspect repository files cannot directly observe all of that Editor state.&lt;/p&gt;

&lt;p&gt;Unity's official &lt;strong&gt;Unity MCP Server&lt;/strong&gt; is intended to close that gap. It allows an external MCP client to connect to the Unity Editor and work with capabilities related to Scenes, GameObjects, Assets, scripts, and the Console.&lt;/p&gt;

&lt;p&gt;This article explains how to start using the official Unity MCP Server from Codex without immediately giving the model broad write access. It focuses on connection setup, permission boundaries, failure recovery, and a repeatable evaluation method built around three tasks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the Unity Console and apply a minimal compile-error fix.&lt;/li&gt;
&lt;li&gt;Create, edit, and save a disposable sandbox Scene.&lt;/li&gt;
&lt;li&gt;Create a Prefab with a precisely defined structure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The order is deliberate. Start with a mostly read-oriented Console workflow, continue with a disposable Scene, and only then create a persistent Prefab Asset. Each stage should proceed only after the previous stage proves that the target Editor, exposed tools, and verification loop are under control.&lt;/p&gt;

&lt;p&gt;The goal is not to watch one successful demo and conclude that the integration is production-ready. A useful evaluation must distinguish first-attempt success, automatic recovery, human intervention, unintended changes, timeouts, and Editor hangs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; This article does not contain measured success rates or elapsed-time results. It is an evaluation and safety-design guide based on official documentation checked on July 21, 2026. Paths, UI labels, and described behavior come from documentation rather than the author's own execution results. Decide whether the integration is suitable for production only after measuring it with fixed versions and a representative project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What “Unity's Official MCP” Means Here
&lt;/h2&gt;

&lt;p&gt;This article covers the &lt;strong&gt;Unity MCP Server&lt;/strong&gt; included in the &lt;code&gt;com.unity.ai.assistant&lt;/code&gt; package. It does not cover community MCP servers with similar names.&lt;/p&gt;

&lt;p&gt;As of July 21, 2026, Unity's documentation describes Assistant &lt;code&gt;2.14.0-pre.1&lt;/code&gt; and requires Unity 6 (&lt;code&gt;6000.0&lt;/code&gt;) or later. Unity AI is in Open Beta, and the setup requires a project connected to Unity Cloud plus an active Trial or Subscription. Unity's &lt;a href="https://unity.com/blog/unity-ai-mcp-how-to-get-started" rel="noopener noreferrer"&gt;official introductory blog&lt;/a&gt; states that using the MCP Server itself does not consume Unity AI credits.&lt;/p&gt;

&lt;p&gt;Because this is Beta and pre-release software, pin the package version used for every evaluation.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.unity3d.com/Packages/com.unity.ai.assistant@2.14/manual/integration/unity-mcp-overview.html" rel="noopener noreferrer"&gt;official Unity MCP overview&lt;/a&gt; describes this architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Codex (MCP client)
  │ MCP over stdio
Unity Relay (~/.unity/relay/)
  │ local IPC
  │ Windows: named pipe
  │ macOS/Linux: Unix socket
Unity Editor (MCP Bridge)
  ├─ built-in tools
  └─ custom tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codex starts the Relay process. The Relay then connects to the MCP Bridge running inside the Unity Editor.&lt;/p&gt;

&lt;p&gt;In Assistant &lt;code&gt;2.14.0-pre.1&lt;/code&gt;, the following menu entries point in opposite directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AI &amp;gt; Assistant MCP Extensions&lt;/code&gt;: Unity Assistant consumes external MCP servers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AI &amp;gt; Unity MCP Server&lt;/code&gt;: Unity exposes Unity capabilities to an external client such as Codex.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction is easy to miss.&lt;/p&gt;

&lt;p&gt;The labels also vary between package versions. For example, Assistant &lt;code&gt;2.7.0-pre.3&lt;/code&gt; documentation used names such as &lt;code&gt;AI &amp;gt; MCP Client&lt;/code&gt; and &lt;code&gt;AI &amp;gt; Unity MCP&lt;/code&gt;. When following older screenshots or blog posts, identify the direction of the connection instead of relying on an exact menu string.&lt;/p&gt;

&lt;p&gt;There is another naming distinction worth preserving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Unity_ManageScene&lt;/code&gt; and &lt;code&gt;Unity_ReadConsole&lt;/code&gt; are examples of tool names exposed to the client.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_components&lt;/code&gt; is described in Unity's troubleshooting documentation as an operation name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not assume an operation belongs to a particular tool without checking the schema discovered by Codex in the actual environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freeze the Evaluation Environment
&lt;/h2&gt;

&lt;p&gt;MCP behavior can change with the Unity version, Assistant package, Codex version, operating system, and project state. A success rate without that context is not useful.&lt;/p&gt;

&lt;p&gt;Record at least the following:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OS&lt;/td&gt;
&lt;td&gt;Windows 11 24H2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;6000.x.xf1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assistant package&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2.14.0-pre.1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;Exact CLI or IDE-extension version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Exact model selected for the run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project&lt;/td&gt;
&lt;td&gt;New Unity 6 URP project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git state&lt;/td&gt;
&lt;td&gt;Reset to the same commit before every attempt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open Unity Editors&lt;/td&gt;
&lt;td&gt;One, or explicitly identify the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Console state&lt;/td&gt;
&lt;td&gt;Cleared before each run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP debug logs&lt;/td&gt;
&lt;td&gt;Enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use a small evaluation repository or a disposable worktree, not a copy of an active production project.&lt;/p&gt;

&lt;p&gt;Every attempt should begin from the same commit, Scene, Console state, and permission configuration. Keep the attempt number, prompt, tool calls, debug logs, and Git diff linked together.&lt;/p&gt;

&lt;p&gt;Do not mix ordinary runs that reuse &lt;code&gt;Library&lt;/code&gt; with runs that intentionally clear caches. Those are different conditions and should be reported separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unity-Side Setup
&lt;/h2&gt;

&lt;p&gt;The basic requirements are Unity 6 or later and the &lt;code&gt;com.unity.ai.assistant&lt;/code&gt; package.&lt;/p&gt;

&lt;p&gt;After opening the Editor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;Edit &amp;gt; Project Settings &amp;gt; AI &amp;gt; Unity MCP Server&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Confirm that &lt;code&gt;Unity Bridge&lt;/code&gt; reports &lt;code&gt;Running&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enable &lt;code&gt;Show Debug Logs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;Validation Level&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enable only the tools required for the current task.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://docs.unity3d.com/Packages/com.unity.ai.assistant@2.14/manual/integration/unity-mcp-get-started.html" rel="noopener noreferrer"&gt;official setup documentation&lt;/a&gt; for Assistant &lt;code&gt;2.14.0-pre.1&lt;/code&gt; states that the Bridge normally starts when the Editor loads and that the Relay executable is placed under the user's &lt;code&gt;.unity/relay&lt;/code&gt; directory. Treat that as documented behavior, then verify the &lt;code&gt;Running&lt;/code&gt; state and actual Relay file in the target environment.&lt;/p&gt;

&lt;p&gt;If the Bridge remains &lt;code&gt;Stopped&lt;/code&gt;, inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unity Console compile errors&lt;/li&gt;
&lt;li&gt;Package installation state&lt;/li&gt;
&lt;li&gt;Whether the Relay binary exists&lt;/li&gt;
&lt;li&gt;MCP debug logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Assistant &lt;code&gt;2.14.0-pre.1&lt;/code&gt;, &lt;code&gt;Validation Level&lt;/code&gt; controls the validation strength of &lt;code&gt;Unity_ManageScript&lt;/code&gt; and offers &lt;code&gt;basic&lt;/code&gt;, &lt;code&gt;standard&lt;/code&gt;, &lt;code&gt;comprehensive&lt;/code&gt;, and &lt;code&gt;strict&lt;/code&gt;. For code-editing evaluation, starting with &lt;code&gt;strict&lt;/code&gt; is reasonable, while recording any speed difference.&lt;/p&gt;

&lt;p&gt;This setting does &lt;strong&gt;not&lt;/strong&gt; represent one universal validation level for every Scene and Prefab operation. Confirm its exact scope in the &lt;a href="https://docs.unity3d.com/Packages/com.unity.ai.assistant@2.14/manual/reference/unity-mcp-reference.html" rel="noopener noreferrer"&gt;configuration reference&lt;/a&gt; for the package version being tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Register the Unity MCP Server in Codex
&lt;/h2&gt;

&lt;p&gt;According to the &lt;a href="https://developers.openai.com/codex/mcp" rel="noopener noreferrer"&gt;Codex MCP documentation&lt;/a&gt;, Codex can register a stdio MCP server in &lt;code&gt;~/.codex/config.toml&lt;/code&gt; or in &lt;code&gt;.codex/config.toml&lt;/code&gt; inside a trusted project.&lt;/p&gt;

&lt;p&gt;For the first connection check, expose only &lt;code&gt;Unity_ReadConsole&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[mcp_servers.unity]&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/absolute/path/to/unity-relay"&lt;/span&gt;
&lt;span class="py"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;"--mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"--project-path"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"/absolute/path/to/UnityMcpLab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;startup_timeout_sec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="py"&gt;tool_timeout_sec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;
&lt;span class="py"&gt;default_tools_approval_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"prompt"&lt;/span&gt;
&lt;span class="py"&gt;enabled_tools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Unity_ReadConsole"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example above uses operating-system-neutral placeholders. Replace both paths with absolute paths from the evaluation machine.&lt;/p&gt;

&lt;p&gt;On Windows, TOML literal strings make paths easier to read because backslashes do not need to be doubled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'C:\Users\name\.unity\relay\relay_win.exe'&lt;/span&gt;
&lt;span class="py"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"--mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--project-path"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'C:\work\UnityMcpLab'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Assistant &lt;code&gt;2.14.0-pre.1&lt;/code&gt; documentation provides the following Relay path examples. They are documentation examples, not observations from this article's author. Confirm them with &lt;code&gt;Locate Server&lt;/code&gt; and the actual filesystem.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OS&lt;/th&gt;
&lt;th&gt;Documented example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS Apple Silicon&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.unity/relay/relay_mac_arm64.app/Contents/MacOS/relay_mac_arm64&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS Intel&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.unity/relay/relay_mac_x64.app/Contents/MacOS/relay_mac_x64&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%USERPROFILE%\.unity\relay\relay_win.exe&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.unity/relay/relay_linux&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Some clients do not expand &lt;code&gt;~&lt;/code&gt;, so use an absolute path for &lt;code&gt;command&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The server can also be registered with the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex mcp add unity &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /absolute/path/to/unity-relay &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mcp&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project-path&lt;/span&gt; /absolute/path/to/UnityMcpLab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; This CLI command registers the MCP server only. It does not establish the read-only tool allowlist shown above. After running it, open the resulting &lt;code&gt;config.toml&lt;/code&gt;—normally &lt;code&gt;~/.codex/config.toml&lt;/code&gt;—and explicitly add or verify &lt;code&gt;enabled_tools = ["Unity_ReadConsole"]&lt;/code&gt; and &lt;code&gt;default_tools_approval_mode = "prompt"&lt;/code&gt; before starting the evaluation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After registration, use &lt;code&gt;codex mcp list&lt;/code&gt; and &lt;code&gt;/mcp&lt;/code&gt; in the interactive session to inspect the connection and the tools actually exposed to Codex.&lt;/p&gt;

&lt;p&gt;A useful permission progression is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Tools exposed to Codex&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Connection check&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Unity_ReadConsole&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;Verify target Editor, approval, Relay, and Console access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Script fix&lt;/td&gt;
&lt;td&gt;Console read plus the script-editing tool found in the environment&lt;/td&gt;
&lt;td&gt;Apply one minimal code change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scene edit&lt;/td&gt;
&lt;td&gt;Scene and GameObject tools found in the environment&lt;/td&gt;
&lt;td&gt;Edit only a sandbox Scene&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefab creation&lt;/td&gt;
&lt;td&gt;GameObject/Component tools plus Asset/Prefab saving tools found in the environment&lt;/td&gt;
&lt;td&gt;Save one Prefab under an approved folder&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The labels “script-editing tool,” “Scene/GameObject tool,” and “Asset/Prefab-saving tool” are conceptual descriptions. They are not guaranteed official tool names. Use the Unity Tools list and the schemas discovered through &lt;code&gt;/mcp&lt;/code&gt;, then copy only the exact names shown in that environment into &lt;code&gt;enabled_tools&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Do not guess a Prefab-saving tool name from an article. If the built-in tools cannot satisfy the requirement, record the task as unsupported with built-ins and evaluate a Custom Tool separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Always Select the Target When Multiple Editors Are Open
&lt;/h2&gt;

&lt;p&gt;According to the Assistant &lt;code&gt;2.14.0-pre.1&lt;/code&gt; setup guide, the Relay may connect to the first Unity Editor it discovers when no target is specified.&lt;/p&gt;

&lt;p&gt;That is dangerous for developers who keep multiple projects, samples, or test Editors open at the same time.&lt;/p&gt;

&lt;p&gt;Select the target with one of these approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--project-path &amp;lt;path&amp;gt;&lt;/code&gt; or &lt;code&gt;UNITY_PROJECT_PATH&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--instance-id &amp;lt;pid&amp;gt;&lt;/code&gt; or &lt;code&gt;UNITY_INSTANCE_ID&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prefer &lt;code&gt;--project-path&lt;/code&gt; for a persistent configuration. A process ID changes whenever the Editor restarts, so it is more useful for temporary diagnosis than long-lived setup.&lt;/p&gt;

&lt;p&gt;“Edit the test Scene” is not a safe instruction if Codex might be connected to another client's project.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Connected Clients&lt;/code&gt; is the UI label used in the documentation for the target package version. During evaluation, capture both the Codex-side connection state and the Unity-side client display so that the selected project can be verified later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approve the First Connection in Unity
&lt;/h2&gt;

&lt;p&gt;The official flow requires Unity-side approval for the first direct connection from an external MCP client.&lt;/p&gt;

&lt;p&gt;The Assistant &lt;code&gt;2.14.0-pre.1&lt;/code&gt; package documentation says that clients appear under &lt;code&gt;Pending Connections&lt;/code&gt;, where &lt;code&gt;Allow&lt;/code&gt; approves the connection and &lt;code&gt;Revoke Access&lt;/code&gt; rejects it. An approved client can reconnect automatically in later sessions.&lt;/p&gt;

&lt;p&gt;Unity's May 11, 2026 blog post uses &lt;code&gt;Accept&lt;/code&gt; for the same approval action. Other versions may also use different page or button labels.&lt;/p&gt;

&lt;p&gt;Do not search only for one exact word. Find the approval action for the client shown under the target version's pending-connections UI, and record the actual label used by the installed version.&lt;/p&gt;

&lt;p&gt;This approval boundary matters, but it is not fine-grained authorization. It means “this Codex client may connect.” It does not by itself mean “Prefab creation is allowed but Scene deletion is forbidden.” Operation-level limits need additional layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classify Results into Four Outcomes
&lt;/h2&gt;

&lt;p&gt;Do not use the model's “Done” message as the success criterion. Judge the result from Unity's actual state.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First-attempt success&lt;/td&gt;
&lt;td&gt;The initial instruction satisfies every condition without an extra save or verification instruction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automatic recovery&lt;/td&gt;
&lt;td&gt;The model re-observes the failure and completes the task within at most two additional tool calls, without human intervention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human-assisted success&lt;/td&gt;
&lt;td&gt;A person selects, deletes, restarts the Bridge, edits directly, or otherwise intervenes before completion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure&lt;/td&gt;
&lt;td&gt;Retry limit exceeded, unintended change occurred, Editor stopped, or the run was rolled back with Git&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not merge human-assisted success into automatic success. A final state that a person repaired does not erase the failed automation attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metrics to Record
&lt;/h2&gt;

&lt;p&gt;Run each task ten times from the same initial state. Allow no more than two recovery attempts. Stop a run immediately after an unintended change, target mismatch, or Editor hang.&lt;/p&gt;

&lt;p&gt;Record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First-attempt success&lt;/li&gt;
&lt;li&gt;Automatic recovery&lt;/li&gt;
&lt;li&gt;Human intervention&lt;/li&gt;
&lt;li&gt;Elapsed time&lt;/li&gt;
&lt;li&gt;Number of tool calls&lt;/li&gt;
&lt;li&gt;Timeouts&lt;/li&gt;
&lt;li&gt;Out-of-scope diffs&lt;/li&gt;
&lt;li&gt;Editor hangs or crashes&lt;/li&gt;
&lt;li&gt;Number of changed files&lt;/li&gt;
&lt;li&gt;Tool configuration: built-in or Custom Tool&lt;/li&gt;
&lt;li&gt;Exact tool names and important arguments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following formulas keep the categories separate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First-attempt success rate = first-attempt successes / all attempts
Recovery-inclusive success rate =
    (first-attempt successes + automatic recoveries) / all attempts
Human-intervention rate = human-assisted attempts / all attempts
Side-effect rate = attempts with unintended changes / all attempts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten runs are not enough to estimate universal model performance. They are a small, repeatable unit for discovering serious failures in one environment and comparing package or configuration changes.&lt;/p&gt;

&lt;p&gt;Do not combine runs that use different allowlists, cache conditions, or built-in/Custom Tool configurations into the same denominator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluation Task 1: Read the Console and Fix a Compile Error
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Start with a read-only connection check
&lt;/h3&gt;

&lt;p&gt;Before allowing any code edit, keep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;enabled_tools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Unity_ReadConsole"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask Codex to summarize the number of Errors and Warnings and show the first few entries. Do not count this connection check among the ten code-fix attempts.&lt;/p&gt;

&lt;p&gt;Proceed only after confirming all of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The correct Editor is connected.&lt;/li&gt;
&lt;li&gt;Unity has approved the client.&lt;/li&gt;
&lt;li&gt;Codex discovers only &lt;code&gt;Unity_ReadConsole&lt;/code&gt; for this stage.&lt;/li&gt;
&lt;li&gt;Console reading works.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then add the exact script-editing tool discovered in the environment to the allowlist.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test input
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;Assets/MCPTest/Scripts/Health.cs&lt;/code&gt; with one missing semicolon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Health&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxHp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;CurrentHp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CurrentHp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxHp&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The challenge is not C# syntax. It is whether the workflow can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the Unity Console.&lt;/li&gt;
&lt;li&gt;Identify the correct source location.&lt;/li&gt;
&lt;li&gt;Apply the smallest possible change.&lt;/li&gt;
&lt;li&gt;Wait for Unity to import and compile.&lt;/li&gt;
&lt;li&gt;Read the Console again.&lt;/li&gt;
&lt;li&gt;Confirm that compile Errors are zero.&lt;/li&gt;
&lt;li&gt;Leave unrelated Warnings untouched.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read the Unity Console and apply only the minimum change required to fix the compile error.

Constraints:
- Do not change the API design, names, fields, or access modifiers.
- Do not fix Warnings unrelated to the Error.
- Wait for Unity compilation to finish.
- Read the Console again and verify that compile Errors are zero.
- Report the changed file and line.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When the Bridge cannot start
&lt;/h3&gt;

&lt;p&gt;Unity's &lt;a href="https://docs.unity3d.com/Packages/com.unity.ai.assistant@2.14/manual/troubleshoot/unity-mcp-troubleshooting.html" rel="noopener noreferrer"&gt;MCP troubleshooting documentation&lt;/a&gt; notes that compile errors can prevent the MCP Bridge from starting. The evaluation therefore needs two explicit paths:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bridge state&lt;/th&gt;
&lt;th&gt;Procedure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Running&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Unity_ReadConsole&lt;/code&gt; → minimal edit → wait for compile → read Console again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stopped&lt;/td&gt;
&lt;td&gt;Use Codex's ordinary repository editing to fix only the blocking error → let Unity compile → start the Bridge → read Console&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It is acceptable to divide responsibilities: normal file editing for the source change, Unity MCP for observing the Editor result.&lt;/p&gt;

&lt;p&gt;Success requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero compile Errors after recompilation&lt;/li&gt;
&lt;li&gt;No diff beyond the expected semicolon&lt;/li&gt;
&lt;li&gt;A second Console read after compilation&lt;/li&gt;
&lt;li&gt;The correct recovery path for the Bridge state&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use two difficulty stages
&lt;/h3&gt;

&lt;p&gt;The missing semicolon is Stage A: it verifies the connection and feedback loop.&lt;/p&gt;

&lt;p&gt;Stage B should use a slightly more semantic but still deterministic error. For example, assign &lt;code&gt;GetComponent&amp;lt;Rigidbody2D&amp;gt;()&lt;/code&gt; to a &lt;code&gt;Rigidbody&lt;/code&gt; field and define the only accepted fix as &lt;code&gt;GetComponent&amp;lt;Rigidbody&amp;gt;()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Predefine the expected diff and acceptance conditions. Do not use an ambiguous bug when evaluating the reliability of the Editor loop.&lt;/p&gt;

&lt;p&gt;Saving the file is not completion. Observe Asset Import, compilation, and the post-Domain-Reload Console state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluation Task 2: Edit a Sandbox Scene
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Required Scene
&lt;/h3&gt;

&lt;p&gt;Create and save:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Assets/MCPTest/Scenes/MCP_Sandbox.unity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with this Hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MCP_Sandbox (Scene)
├─ Environment
│  ├─ Floor
│  └─ Directional Light
├─ Main Camera
└─ SpawnPoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Acceptance conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Scene is saved at the exact path.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Environment&lt;/code&gt;, &lt;code&gt;Main Camera&lt;/code&gt;, and &lt;code&gt;SpawnPoint&lt;/code&gt; are at the Scene root.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Environment&lt;/code&gt; has Position and Rotation set to zero and Scale set to one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Floor&lt;/code&gt; is a child of &lt;code&gt;Environment&lt;/code&gt; and has Cube rendering Components plus a &lt;code&gt;BoxCollider&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Floor.localPosition = (0, -0.5, 0)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Floor.localScale = (10, 1, 10)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Directional Light&lt;/code&gt; has a &lt;code&gt;Light&lt;/code&gt; Component with the Directional type.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SpawnPoint&lt;/code&gt; has only a Transform and &lt;code&gt;position = (0, 1, -4)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Main Camera&lt;/code&gt; has a &lt;code&gt;Camera&lt;/code&gt; Component and the &lt;code&gt;MainCamera&lt;/code&gt; tag.&lt;/li&gt;
&lt;li&gt;The Scene is saved and no other Scene changes.&lt;/li&gt;
&lt;li&gt;The Console has no Errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use Unity MCP to create Assets/MCPTest/Scenes/MCP_Sandbox.unity with the following state.

Hierarchy:
- Environment
  - Floor
  - Directional Light
- Main Camera
- SpawnPoint

Settings:
- Environment Position/Rotation = 0 and Scale = 1.
- Create Floor as a Cube with localPosition = (0, -0.5, 0)
  and localScale = (10, 1, 10).
- Directional Light must contain a Directional Light component.
- SpawnPoint must contain only a Transform and use position = (0, 1, -4).
- Main Camera must contain a Camera and use Tag = MainCamera.

Constraints:
- Do not edit an existing Scene.
- If a Scene already exists at the target path, stop without overwriting it.
- UI and Canvas objects are outside this evaluation task.

After saving, re-read the active Scene path, Hierarchy, Components, Transforms,
Light Type, Camera Tag, save state, and Console Errors.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What to inspect
&lt;/h3&gt;

&lt;p&gt;A Scene may look correct while still being dirty and unsaved. After saving, re-read the Scene path and Hierarchy. In a multi-Scene setup, verify the Active Scene and the owning Scene of every created GameObject.&lt;/p&gt;

&lt;p&gt;World and local coordinates must remain explicit. Parenting can change the effective result, so the prompt and the acceptance test should both distinguish &lt;code&gt;position&lt;/code&gt; from &lt;code&gt;localPosition&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exclude UI Hierarchies for This Version
&lt;/h2&gt;

&lt;p&gt;Unity's &lt;a href="https://docs.unity3d.com/Packages/com.unity.ai.assistant@2.14/manual/troubleshoot/unity-mcp-troubleshooting.html" rel="noopener noreferrer"&gt;troubleshooting documentation&lt;/a&gt; for Assistant &lt;code&gt;2.14.0-pre.1&lt;/code&gt; warns that running the documented &lt;code&gt;get_components&lt;/code&gt; operation on GameObjects in a Canvas or UI hierarchy may stop or crash the Editor. The listed affected Components include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Canvas&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CanvasScaler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GraphicRaycaster&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RectTransform&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;get_components&lt;/code&gt; is the operation name used in the documentation. Confirm which discovered tool and input schema produce that operation in the installed version.&lt;/p&gt;

&lt;p&gt;For this evaluation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not retrieve Components from UI hierarchies.&lt;/li&gt;
&lt;li&gt;Do not broadly enumerate the entire Scene.&lt;/li&gt;
&lt;li&gt;Limit queries to a known root.&lt;/li&gt;
&lt;li&gt;Save every tool call and its arguments.&lt;/li&gt;
&lt;li&gt;If the Editor hangs, identify the last call and stop the run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add UI only after the relevant Issue Tracker entry and release notes show that the problem is fixed for the pinned version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluation Task 3: Create a Prefab
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Expand the allowlist first
&lt;/h3&gt;

&lt;p&gt;The read-only configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;enabled_tools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Unity_ReadConsole"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;cannot create a Prefab.&lt;/p&gt;

&lt;p&gt;Before the task, inspect the actual environment and add only the tools required for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GameObject creation&lt;/li&gt;
&lt;li&gt;Component configuration&lt;/li&gt;
&lt;li&gt;Asset or Prefab saving&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If no built-in saving tool can satisfy the task, record “unsupported with built-in tools” and move the Custom Tool attempt into a separate result group.&lt;/p&gt;

&lt;h3&gt;
  
  
  Required Prefab
&lt;/h3&gt;

&lt;p&gt;Create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Assets/MCPTest/Prefabs/Crate.prefab

Crate
├─ Visual
└─ Handle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Acceptance conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Root name is &lt;code&gt;Crate&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The root contains only Transform, &lt;code&gt;BoxCollider&lt;/code&gt;, and &lt;code&gt;Rigidbody&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Rigidbody.mass = 10&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Visual&lt;/code&gt; contains only Transform, &lt;code&gt;MeshFilter&lt;/code&gt;, and &lt;code&gt;MeshRenderer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Visual&lt;/code&gt; does not retain a &lt;code&gt;BoxCollider&lt;/code&gt; added by &lt;code&gt;CreatePrimitive&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Handle&lt;/code&gt; contains only a Transform.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Handle.localPosition = (0, 0.6, 0)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Prefab is saved at the exact path.&lt;/li&gt;
&lt;li&gt;No temporary GameObject remains in the Scene.&lt;/li&gt;
&lt;li&gt;Reopening the saved Prefab preserves the structure and values.&lt;/li&gt;
&lt;li&gt;The Console has no Errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use Unity MCP to create Assets/MCPTest/Prefabs/Crate.prefab.

Requirements:
- Root name: Crate
- Add BoxCollider and Rigidbody to the root.
- Set Rigidbody.mass to 10.
- Child Visual must contain only the Cube MeshFilter and MeshRenderer.
- If CreatePrimitive added a BoxCollider to Visual, remove it before saving.
- Child Handle must contain only a Transform.
- Set Handle.localPosition to (0, 0.6, 0).
- Do not leave a temporary GameObject in the Scene.
- If a Prefab already exists at the target path, stop without overwriting it.

After saving, re-read the Prefab and report the Hierarchy, Components, mass,
localPosition, save path, and Console Errors. Do not classify the task as
successful if any unspecified Component remains.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The evaluated workflow includes creation, configuration, save, cleanup, and re-open verification.&lt;/p&gt;

&lt;p&gt;Treat all of the following as failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A temporary Scene object remains.&lt;/li&gt;
&lt;li&gt;A required value was never set.&lt;/li&gt;
&lt;li&gt;World and local coordinates were confused.&lt;/li&gt;
&lt;li&gt;An existing Asset was overwritten.&lt;/li&gt;
&lt;li&gt;An extra Collider remains on &lt;code&gt;Visual&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The saved Prefab was never reopened and checked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After a failed attempt, observe the current Asset and Scene state before changing anything. Repair one condition at a time.&lt;/p&gt;

&lt;p&gt;For repeated production use, consider replacing a series of generic calls with a narrow Custom Tool such as &lt;code&gt;CreateOrValidatePrefab&lt;/code&gt; that provides a &lt;code&gt;dryRun&lt;/code&gt; mode, path allowlist, and explicit overwrite policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluation Record Template
&lt;/h2&gt;

&lt;p&gt;Copy this table for Console, Scene, and Prefab tasks and fill ten rows for each configuration.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Tool setup&lt;/th&gt;
&lt;th&gt;Tools used&lt;/th&gt;
&lt;th&gt;First&lt;/th&gt;
&lt;th&gt;Recovery&lt;/th&gt;
&lt;th&gt;Human&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Calls&lt;/th&gt;
&lt;th&gt;Timeout&lt;/th&gt;
&lt;th&gt;Out-of-scope diff&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Summarize the results separately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Tool setup&lt;/th&gt;
&lt;th&gt;First-attempt success&lt;/th&gt;
&lt;th&gt;Recovery-inclusive success&lt;/th&gt;
&lt;th&gt;Human-intervention rate&lt;/th&gt;
&lt;th&gt;Side-effect rate&lt;/th&gt;
&lt;th&gt;Median time&lt;/th&gt;
&lt;th&gt;Editor stops&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Console fix&lt;/td&gt;
&lt;td&gt;Built-in / ordinary editing&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scene edit&lt;/td&gt;
&lt;td&gt;Built-in only&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefab creation&lt;/td&gt;
&lt;td&gt;Built-in only&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefab creation&lt;/td&gt;
&lt;td&gt;With Custom Tool&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not report only the average. Record median time, maximum time, timeouts, and Editor stops separately. Keep built-in and Custom Tool runs in different result groups.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preserve evidence
&lt;/h3&gt;

&lt;p&gt;For an internal adoption decision or a public report, save evidence for both successful and failed runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unity MCP Server settings&lt;/li&gt;
&lt;li&gt;Codex tool list&lt;/li&gt;
&lt;li&gt;Prefab Hierarchy and Inspector&lt;/li&gt;
&lt;li&gt;Console before and after the code fix&lt;/li&gt;
&lt;li&gt;Scene Hierarchy after editing&lt;/li&gt;
&lt;li&gt;Debug logs or tool-call details for failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Associate every screenshot and log with a run number. Before publishing, mask user names, absolute paths, project names, credentials, and other confidential data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Suggested pilot thresholds
&lt;/h3&gt;

&lt;p&gt;The following are proposed internal thresholds, not Unity or OpenAI requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At least 80% first-attempt success&lt;/li&gt;
&lt;li&gt;At least 95% success including automatic recovery&lt;/li&gt;
&lt;li&gt;Zero unintended changes&lt;/li&gt;
&lt;li&gt;Zero Editor hangs or crashes&lt;/li&gt;
&lt;li&gt;No more than one human-assisted run per ten attempts&lt;/li&gt;
&lt;li&gt;Faster median completion than the same task performed manually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Scene and Prefab work, side effects matter more than speed. Nine fast successes do not compensate for one run that damages another Scene.&lt;/p&gt;

&lt;p&gt;For Console fixes, give the highest weight to a minimal diff and post-compilation verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define Recovery as a State Machine
&lt;/h2&gt;

&lt;p&gt;A free-form “try again” prompt makes the recovery process different on every run. Fix the workflow to a small state machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observe → Plan → Act one step → Wait → Verify
   ↑                                  │
   └──────── known temporary failure ─┘
                                      ├─ success: Stop
                                      └─ destructive risk: Stop and roll back
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Observe&lt;/td&gt;
&lt;td&gt;Read the Active Scene, target Asset, Hierarchy, Console, Bridge state, and Git diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan&lt;/td&gt;
&lt;td&gt;Choose one next operation and define the state that should follow it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Act one step&lt;/td&gt;
&lt;td&gt;Limit the change to one GameObject, one Component, or one-file diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wait&lt;/td&gt;
&lt;td&gt;Wait for import, compilation, Domain Reload, or save completion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verify&lt;/td&gt;
&lt;td&gt;Judge the acceptance conditions from newly retrieved Unity state and Git diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stop / roll back&lt;/td&gt;
&lt;td&gt;Stop immediately after target mismatch, unintended change, Editor stop, or overwrite risk&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After a timeout, return to &lt;strong&gt;Observe&lt;/strong&gt;. Do not immediately send the same write operation again.&lt;/p&gt;

&lt;p&gt;Common stop conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The connected project or target Scene is not the expected one.&lt;/li&gt;
&lt;li&gt;An existing Asset may be overwritten.&lt;/li&gt;
&lt;li&gt;A non-target Scene, Asset, or script changed.&lt;/li&gt;
&lt;li&gt;The Unity Editor stopped or crashed and the final call cannot be identified.&lt;/li&gt;
&lt;li&gt;State cannot be re-read after a timeout, so execution status is unknown.&lt;/li&gt;
&lt;li&gt;An unapproved tool or Custom Tool was called.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Task-specific sections should add only their own acceptance conditions instead of redefining this common list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Four Permission Layers
&lt;/h2&gt;

&lt;p&gt;Unity's connection approval alone is not sufficient. Combine four layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Git and the work environment
&lt;/h3&gt;

&lt;p&gt;Use a dedicated branch or worktree, a clean baseline commit, and one task per commit. Begin with a sandbox Scene rather than a production Scene. Inspect the result in Unity as well as in serialized YAML diffs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Codex sandbox and approvals
&lt;/h3&gt;

&lt;p&gt;The Codex sandbox controls local command access. Approval policy controls when human confirmation is required.&lt;/p&gt;

&lt;p&gt;Unity MCP tool calls are a separate path from shell editing, so the sandbox is not the only boundary. Do not begin with &lt;code&gt;sandbox_mode = "danger-full-access"&lt;/code&gt; or disabled approval. For shell operations, start with &lt;code&gt;--sandbox workspace-write --ask-for-approval on-request&lt;/code&gt; or the equivalent configuration, while keeping MCP tool approval in &lt;code&gt;prompt&lt;/code&gt; mode.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: Codex MCP tool restrictions
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;enabled_tools&lt;/code&gt;, &lt;code&gt;disabled_tools&lt;/code&gt;, and approval modes to expose only the tools needed for the current task.&lt;/p&gt;

&lt;p&gt;For the connection check, expose only &lt;code&gt;Unity_ReadConsole&lt;/code&gt;. For write operations, add only exact tool names verified through the installed environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 4: Unity MCP Server and narrow Custom Tools
&lt;/h3&gt;

&lt;p&gt;Disable unnecessary tools on the Unity side as well.&lt;/p&gt;

&lt;p&gt;For repeated workflows, prefer a narrow Custom Tool that enforces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Approved folders&lt;/li&gt;
&lt;li&gt;Approved Scenes&lt;/li&gt;
&lt;li&gt;Overwrite policy&lt;/li&gt;
&lt;li&gt;Maximum object count&lt;/li&gt;
&lt;li&gt;Dry-run support&lt;/li&gt;
&lt;li&gt;Undo&lt;/li&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples include &lt;code&gt;CreateEnemySpawnPoint&lt;/code&gt; or &lt;code&gt;ValidatePrefabReferences&lt;/code&gt; rather than a generic “modify anything” endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local IPC Does Not Mean All Data Stays Local
&lt;/h2&gt;

&lt;p&gt;The Relay-to-Editor connection uses local IPC, but Codex reasoning may still involve remote services.&lt;/p&gt;

&lt;p&gt;Before using the integration with production data, review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI plan and workspace data controls&lt;/li&gt;
&lt;li&gt;Unity AI terms and data settings&lt;/li&gt;
&lt;li&gt;Client confidentiality requirements&lt;/li&gt;
&lt;li&gt;Whether Console logs may contain credentials or personal data&lt;/li&gt;
&lt;li&gt;Storage location and retention of MCP debug logs and Codex history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;“Communication with the Editor is local” is not enough to conclude that confidential project data can be sent through the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be Careful with Batch Mode Auto-Approval
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Auto-approve in Batch Mode&lt;/code&gt; allows an MCP client running in Batch Mode to connect without the normal interactive approval step.&lt;/p&gt;

&lt;p&gt;That can be useful in CI, but it removes an important boundary. Do not enable it at the beginning of an evaluation.&lt;/p&gt;

&lt;p&gt;A safe progression is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Human-supervised local evaluation&lt;/li&gt;
&lt;li&gt;Read-only automation&lt;/li&gt;
&lt;li&gt;Narrow write operations in an isolated runner&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If Batch Mode auto-approval is eventually enabled, require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disposable runners&lt;/li&gt;
&lt;li&gt;A fixed repository and fixed write locations&lt;/li&gt;
&lt;li&gt;A strict tool allowlist&lt;/li&gt;
&lt;li&gt;Automated diff validation&lt;/li&gt;
&lt;li&gt;No direct writes to the main branch&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Custom Tools Are the Path to Production Use
&lt;/h2&gt;

&lt;p&gt;Built-in tools are useful for small experiments. Repeated production workflows are often safer when they are consolidated into narrow Custom Tools registered through Unity's &lt;a href="https://docs.unity3d.com/Packages/com.unity.ai.assistant@2.14/manual/integration/unity-mcp-tool-registration.html" rel="noopener noreferrer"&gt;official extension mechanism&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For Prefab creation, instead of allowing the model to compose many generic operations, expose something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CreateValidatedPrefab(sourceDefinition, outputPath, overwritePolicy, dryRun)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its implementation can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate the output path and overwrite policy.&lt;/li&gt;
&lt;li&gt;Create and configure objects inside one Undo group.&lt;/li&gt;
&lt;li&gt;Save the Prefab.&lt;/li&gt;
&lt;li&gt;Reopen the saved Asset.&lt;/li&gt;
&lt;li&gt;Run a project-specific validator.&lt;/li&gt;
&lt;li&gt;Destroy temporary objects.&lt;/li&gt;
&lt;li&gt;Return a structured result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important shift is to treat Unity's official MCP not as “natural language can do anything in the Editor,” but as a standard interface through which an AI can reach carefully designed internal Editor tools.&lt;/p&gt;

&lt;p&gt;That makes the failure surface and ownership boundary much smaller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write Prompts as Input Specifications
&lt;/h2&gt;

&lt;p&gt;The recovery state machine defines what happens after a failure. The prompt defines what is allowed to change.&lt;/p&gt;

&lt;p&gt;A useful prompt specifies five things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;What to include&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Target&lt;/td&gt;
&lt;td&gt;Project, Scene, Asset path, and root GameObject&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Completion&lt;/td&gt;
&lt;td&gt;Save, reopen, values, and Console verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prohibitions&lt;/td&gt;
&lt;td&gt;No overwrite, no other Scene, no UI query, and similar limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change size&lt;/td&gt;
&lt;td&gt;One creation, setting, or code change at a time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification&lt;/td&gt;
&lt;td&gt;Newly retrieved Unity state, not model self-report&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not ask only, “Did it work?”&lt;/p&gt;

&lt;p&gt;Ask for concrete evidence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Reopen the saved Prefab and return its Hierarchy, Components, values, save path, and Console state.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The purpose is to turn an ambiguous natural-language request into executable acceptance criteria.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat Timeouts as Unknown State
&lt;/h2&gt;

&lt;p&gt;Unity tool calls can be delayed while the Editor imports Assets, builds, compiles scripts, or reloads the domain.&lt;/p&gt;

&lt;p&gt;Record these as separate events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Relay startup&lt;/li&gt;
&lt;li&gt;Bridge connection&lt;/li&gt;
&lt;li&gt;Tool discovery&lt;/li&gt;
&lt;li&gt;Import or compilation&lt;/li&gt;
&lt;li&gt;Domain Reload&lt;/li&gt;
&lt;li&gt;Tool-internal hang&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not hide the cause by increasing every timeout indiscriminately.&lt;/p&gt;

&lt;p&gt;A timed-out write call does not prove that nothing happened. The operation may have completed while the response was lost.&lt;/p&gt;

&lt;p&gt;Before retrying, re-read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The target Asset&lt;/li&gt;
&lt;li&gt;The Hierarchy&lt;/li&gt;
&lt;li&gt;The Git diff&lt;/li&gt;
&lt;li&gt;The Console&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repeated Custom Tools should ideally be idempotent through existing-state checks or a request ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Unity's official MCP Server provides an official connection surface from Codex to Unity Editor state, including Scenes, GameObjects, Assets, scripts, and the Console.&lt;/p&gt;

&lt;p&gt;That access is useful, but it is not automatically safe. A production-minded setup must control the target Editor, exposed tools, save behavior, re-read verification, and timeout recovery.&lt;/p&gt;

&lt;p&gt;Before adoption, repeat the Console, sandbox Scene, and Prefab tasks from the same initial state. Measure first-attempt success, automatic recovery, human intervention, unintended changes, and Editor stops separately.&lt;/p&gt;

&lt;p&gt;Begin with read-only Console observation. Expand access only to a disposable Scene, then to Prefab creation. Treat any out-of-scope modification as more important than raw speed.&lt;/p&gt;

&lt;p&gt;Only workflows that meet the team's threshold should become candidates for routine use. Everything else can remain observation-only.&lt;/p&gt;

&lt;p&gt;The most practical mental model is not “let natural language control the whole Editor.” It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Use allowlists and narrow Custom Tools to make Unity MCP a controlled entry point into well-defined Editor automation.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;The following sources were checked on July 21, 2026. Paths, labels, Validation Level values, and known issues in this article are based on these primary sources. Unity AI and the Assistant package are Beta/pre-release software, so recheck the exact version before evaluation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://unity.com/blog/unity-ai-mcp-how-to-get-started" rel="noopener noreferrer"&gt;Unity AI Open Beta: How to get started with MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.ai.assistant@2.7/manual/integration/unity-mcp-overview.html" rel="noopener noreferrer"&gt;AI client integration with Unity (MCP) | Unity Assistant 2.7&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://unity.com/features/ai" rel="noopener noreferrer"&gt;Unity AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.ai.assistant@2.14/manual/integration/unity-mcp-overview.html" rel="noopener noreferrer"&gt;AI client integration with Unity (MCP) | Unity Assistant 2.14&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.ai.assistant@2.14/manual/integration/unity-mcp-get-started.html" rel="noopener noreferrer"&gt;Get started with Unity MCP | Unity Assistant 2.14&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.ai.assistant@2.14/manual/reference/unity-mcp-reference.html" rel="noopener noreferrer"&gt;Unity MCP Server configuration page reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.ai.assistant@2.14/manual/troubleshoot/unity-mcp-troubleshooting.html" rel="noopener noreferrer"&gt;Troubleshoot Unity MCP bridge issues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.ai.assistant@2.14/manual/integration/unity-mcp-tool-registration.html" rel="noopener noreferrer"&gt;Register custom MCP tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/codex/mcp" rel="noopener noreferrer"&gt;Model Context Protocol | Codex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/codex/config-reference" rel="noopener noreferrer"&gt;Codex configuration reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/codex/agent-approvals-security" rel="noopener noreferrer"&gt;Agent approvals and security | Codex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.openai.com/en/articles/5722486-how-your-data-is-used-to-improve-model-performance" rel="noopener noreferrer"&gt;How your data is used to improve model performance | OpenAI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>mcp</category>
      <category>ai</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Unity Data Formats in Practice: When to Use YAML, JSON, MessagePack, or JSON + gzip</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Fri, 17 Jul 2026 03:02:22 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/unity-data-formats-in-practice-when-to-use-yaml-json-messagepack-or-json-gzip-2eoi</link>
      <guid>https://dev.to/gamedevtoollab/unity-data-formats-in-practice-when-to-use-yaml-json-messagepack-or-json-gzip-2eoi</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;A Unity project can start with one small JSON file and remain perfectly healthy for a long time.&lt;/p&gt;

&lt;p&gt;As the project grows, however, the data requirements usually split in different directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some files must be easy for humans to edit.&lt;/li&gt;
&lt;li&gt;Some must produce readable Git diffs.&lt;/li&gt;
&lt;li&gt;Some must load quickly on a mobile device.&lt;/li&gt;
&lt;li&gt;Some must be shared with a server or an operations tool.&lt;/li&gt;
&lt;li&gt;Some must remain compatible with older clients or save files.&lt;/li&gt;
&lt;li&gt;Some must work correctly under IL2CPP and AOT restrictions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trying to satisfy all of those requirements with one format eventually creates conflicts.&lt;/p&gt;

&lt;p&gt;This article compares YAML, JSON, and MessagePack from a production Unity perspective. It focuses on user settings, save data, runtime distribution, server communication, and static game data—often called &lt;em&gt;master data&lt;/em&gt; in Japanese game development.&lt;/p&gt;

&lt;p&gt;The central idea is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Use human-friendly formats at authoring time, machine-friendly formats at runtime, and choose network formats by balancing observability against efficiency.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The question is not which format is universally best. The useful question is who reads the data, who edits it, when it is validated, and where it is consumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practical Answer First
&lt;/h2&gt;

&lt;p&gt;The following table is a useful starting point for many Unity projects.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Good default&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hand-written configuration&lt;/td&gt;
&lt;td&gt;YAML&lt;/td&gt;
&lt;td&gt;Comments, hierarchy, and reviewable diffs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simple external integration or Web API&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;Broad compatibility and excellent tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small Unity user settings&lt;/td&gt;
&lt;td&gt;PlayerPrefs or JSON&lt;/td&gt;
&lt;td&gt;Minimal implementation cost and easy inspection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Save data&lt;/td&gt;
&lt;td&gt;JSON or MessagePack&lt;/td&gt;
&lt;td&gt;Choose between observability and size/parse cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authoring static game data&lt;/td&gt;
&lt;td&gt;Spreadsheet, Excel, YAML, or CSV&lt;/td&gt;
&lt;td&gt;Match the format to the people editing it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime static game data&lt;/td&gt;
&lt;td&gt;MessagePack, generated code, or a project-specific binary&lt;/td&gt;
&lt;td&gt;Favor validated data and efficient loading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initial server API implementation&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;Easier debugging, logging, and integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-volume or high-frequency APIs&lt;/td&gt;
&lt;td&gt;JSON + gzip or MessagePack&lt;/td&gt;
&lt;td&gt;Reduce network size, parse cost, or both&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operations tools, admin panels, and logs&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;Easy for both people and tools to consume&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The common mistake is to make one global rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“YAML is readable, so everything should be YAML.”&lt;/li&gt;
&lt;li&gt;“JSON is standard, so everything should be JSON.”&lt;/li&gt;
&lt;li&gt;“MessagePack is fast, so everything should be MessagePack.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A more useful mental model is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;YAML leans toward human authoring.&lt;/li&gt;
&lt;li&gt;JSON leans toward interoperability and investigation.&lt;/li&gt;
&lt;li&gt;MessagePack leans toward runtime efficiency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to Evaluate Before Choosing a Format
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Will people read or edit it directly?
&lt;/h3&gt;

&lt;p&gt;For hand-edited data, readability and diff quality matter more than raw parsing speed.&lt;/p&gt;

&lt;p&gt;YAML supports comments and can represent nested structures without as much punctuation as JSON. JSON is still reasonably readable, but standard JSON does not allow comments or trailing commas. MessagePack is not intended for direct editing at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much data is there, and how often is it loaded?
&lt;/h3&gt;

&lt;p&gt;For a few small settings, the difference between YAML and JSON is rarely important.&lt;/p&gt;

&lt;p&gt;At tens of thousands of rows or millions of cells, the costs become visible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parsing&lt;/li&gt;
&lt;li&gt;String allocation&lt;/li&gt;
&lt;li&gt;Numeric conversion&lt;/li&gt;
&lt;li&gt;Garbage collection&lt;/li&gt;
&lt;li&gt;File size&lt;/li&gt;
&lt;li&gt;Object construction after deserialization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The acceptable threshold depends on the target device, compression, load frequency, and the shape of the resulting object graph. Measure with representative data instead of relying on format reputation.&lt;/p&gt;

&lt;p&gt;For large runtime datasets, possible options include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MessagePack&lt;/li&gt;
&lt;li&gt;A project-specific binary format&lt;/li&gt;
&lt;li&gt;Generated C# data or loaders&lt;/li&gt;
&lt;li&gt;SQLite&lt;/li&gt;
&lt;li&gt;FlatBuffers&lt;/li&gt;
&lt;li&gt;MemoryPack&lt;/li&gt;
&lt;li&gt;Partitioned ScriptableObject assets loaded through Addressables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MessagePack is a strong option, but it is not the only runtime-oriented format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it network data or a local file?
&lt;/h3&gt;

&lt;p&gt;Network data passes through more systems than a local file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The server&lt;/li&gt;
&lt;li&gt;The client&lt;/li&gt;
&lt;li&gt;Logging and monitoring&lt;/li&gt;
&lt;li&gt;Debug proxies&lt;/li&gt;
&lt;li&gt;Customer-support tooling&lt;/li&gt;
&lt;li&gt;Admin panels&lt;/li&gt;
&lt;li&gt;Automated tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Binary formats can reduce payload and parsing costs, but they also make incident investigation harder.&lt;/p&gt;

&lt;p&gt;It also helps to separate &lt;strong&gt;serialization format&lt;/strong&gt; from &lt;strong&gt;compression&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON can be sent with gzip to reduce transfer size.&lt;/li&gt;
&lt;li&gt;MessagePack can reduce representation size and parsing overhead.&lt;/li&gt;
&lt;li&gt;MessagePack can also be compressed, although that is a separate decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical distinction is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use JSON when compatibility and observability dominate.&lt;/li&gt;
&lt;li&gt;Use JSON + gzip when bandwidth is the main issue and JSON parsing is still acceptable.&lt;/li&gt;
&lt;li&gt;Consider MessagePack when parsing time, allocation, or very frequent traffic is also a bottleneck.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How must schema changes be handled?
&lt;/h3&gt;

&lt;p&gt;Games regularly add fields, rename data, change types, and support older clients or save files.&lt;/p&gt;

&lt;p&gt;The important part is not only the format. It is the &lt;strong&gt;versioning policy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;JSON includes field names, so readers can often ignore unknown fields. MessagePack can behave similarly with map-style string keys, though this reduces some of its size and speed advantages. Array-style integer keys can be more compact, but key-number management becomes part of the compatibility contract.&lt;/p&gt;

&lt;p&gt;YAML is often best treated as an authoring format. Validate and normalize it during conversion, then give the generated runtime data an explicit version.&lt;/p&gt;

&lt;h2&gt;
  
  
  YAML: Strong for Human-Edited Configuration
&lt;/h2&gt;

&lt;p&gt;YAML is designed to be comfortable for people to read and write.&lt;/p&gt;

&lt;p&gt;Typical uses include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build configuration&lt;/li&gt;
&lt;li&gt;CI configuration&lt;/li&gt;
&lt;li&gt;Tool settings&lt;/li&gt;
&lt;li&gt;Environment settings&lt;/li&gt;
&lt;li&gt;Small data definitions&lt;/li&gt;
&lt;li&gt;Conversion rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A stage definition might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stage_001&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Beginning Forest&lt;/span&gt;
    &lt;span class="na"&gt;enemyLevel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;bgm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;forest_day&lt;/span&gt;
    &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;first&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stage&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;after&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tutorial"&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stage_002&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Old Mine&lt;/span&gt;
    &lt;span class="na"&gt;enemyLevel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;
    &lt;span class="na"&gt;bgm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mine&lt;/span&gt;
    &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dark&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;area&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;limited-visibility&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mechanics"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One of YAML's most practical advantages is comments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Early-game stage. Keep enemy attack values low.&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stage_001&lt;/span&gt;
  &lt;span class="na"&gt;enemyLevel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="c1"&gt;# New players tend to struggle here, so give slightly more gold.&lt;/span&gt;
  &lt;span class="na"&gt;rewardGold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In game development, comments such as “why this value exists” or “this is a temporary tuning override” can be more valuable than the syntax itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where YAML fits in a Unity project
&lt;/h3&gt;

&lt;p&gt;YAML is useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configuration edited directly by engineers&lt;/li&gt;
&lt;li&gt;Data that needs comments&lt;/li&gt;
&lt;li&gt;Settings reviewed in Git&lt;/li&gt;
&lt;li&gt;Environment-specific configuration&lt;/li&gt;
&lt;li&gt;Build and CI tooling&lt;/li&gt;
&lt;li&gt;Small or medium hand-written datasets&lt;/li&gt;
&lt;li&gt;Source data converted before the player build&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Addressables helper configuration&lt;/li&gt;
&lt;li&gt;Custom build-pipeline settings&lt;/li&gt;
&lt;li&gt;Data-conversion rules&lt;/li&gt;
&lt;li&gt;Localization conversion rules&lt;/li&gt;
&lt;li&gt;Static-data generation settings&lt;/li&gt;
&lt;li&gt;Development cheat-menu definitions&lt;/li&gt;
&lt;li&gt;Server endpoint definitions by environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This does &lt;strong&gt;not&lt;/strong&gt; mean manually editing Unity's serialized &lt;code&gt;.prefab&lt;/code&gt;, &lt;code&gt;.unity&lt;/code&gt;, or &lt;code&gt;.asset&lt;/code&gt; text files. The YAML discussed here is project-owned configuration or source data.&lt;/p&gt;

&lt;p&gt;Unity does not provide a general-purpose YAML parser comparable to &lt;code&gt;JsonUtility&lt;/code&gt;. A practical YAML workflow usually depends on one of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A library such as YamlDotNet&lt;/li&gt;
&lt;li&gt;A project-specific conversion tool&lt;/li&gt;
&lt;li&gt;A CI conversion step&lt;/li&gt;
&lt;li&gt;An external data pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reading YAML directly at runtime is possible, but it introduces parsing cost, allocations, package size, error handling, and IL2CPP/AOT considerations. In many projects, a safer workflow is to convert YAML into JSON, MessagePack, generated code, or another validated runtime format during the build or CI process.&lt;/p&gt;

&lt;p&gt;YAML is also not automatically the best authoring format for every static dataset. When designers need to edit thousands of rows as a table, a spreadsheet, Excel workbook, or dedicated admin UI is usually more practical.&lt;/p&gt;

&lt;h3&gt;
  
  
  YAML pitfalls
&lt;/h3&gt;

&lt;p&gt;YAML is convenient, but its flexibility can produce surprising behavior.&lt;/p&gt;

&lt;p&gt;Consider these values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;value1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;001&lt;/span&gt;
&lt;span class="na"&gt;value2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="na"&gt;value3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-07-08&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether they are interpreted as strings, numbers, booleans, or dates can depend on the YAML version and parser behavior.&lt;/p&gt;

&lt;p&gt;For example, tools that retain YAML 1.1 behavior may interpret &lt;code&gt;yes&lt;/code&gt; as a boolean. YAML 1.2 narrowed implicit typing, but production projects still need to verify the behavior of the selected parser and surrounding tools.&lt;/p&gt;

&lt;p&gt;Game data often uses IDs and code-like values that must remain strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;characterId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;001"&lt;/span&gt;
&lt;span class="na"&gt;itemId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes"&lt;/span&gt;
&lt;span class="na"&gt;releaseDate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-07-08"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A safe project rule is to quote IDs, date-like values, and codes with leading zeroes.&lt;/p&gt;

&lt;p&gt;YAML also supports anchors and aliases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;defaultEnemy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;defaultEnemy&lt;/span&gt;
  &lt;span class="na"&gt;hp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
  &lt;span class="na"&gt;attack&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
  &lt;span class="na"&gt;defense&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

&lt;span class="na"&gt;enemies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*defaultEnemy&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slime&lt;/span&gt;
    &lt;span class="na"&gt;hp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*defaultEnemy&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;goblin&lt;/span&gt;
    &lt;span class="na"&gt;attack&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can remove duplication, but heavy use makes the final resolved value harder to see during review.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; merge key in this example comes from a YAML 1.1-era extension and is not part of the YAML 1.2 core schema. Support and configuration vary by parser, so confirm the exact behavior before relying on it.&lt;/p&gt;

&lt;p&gt;For production data, it is often wise to restrict the YAML features allowed by the project and emit a normalized representation during conversion.&lt;/p&gt;

&lt;h2&gt;
  
  
  JSON: The Default for Interoperability and Investigation
&lt;/h2&gt;

&lt;p&gt;JSON is a practical general-purpose data-exchange default.&lt;/p&gt;

&lt;p&gt;It is readable text, supported by almost every server language, and deeply integrated into HTTP tooling, browsers, command-line utilities, admin panels, logs, and monitoring systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why JSON remains so useful
&lt;/h3&gt;

&lt;p&gt;JSON is not the most compact or fastest format. Its strength is the surrounding ecosystem.&lt;/p&gt;

&lt;p&gt;When an API fails, a JSON request or response can be inspected directly in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server logs&lt;/li&gt;
&lt;li&gt;Browser developer tools&lt;/li&gt;
&lt;li&gt;&lt;code&gt;curl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Postman&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jq&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Monitoring dashboards&lt;/li&gt;
&lt;li&gt;Automated scripts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes JSON a very practical starting point for game-server APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON limitations
&lt;/h3&gt;

&lt;p&gt;JSON has several weaknesses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standard JSON has no comments.&lt;/li&gt;
&lt;li&gt;Binary data must be encoded separately, often with Base64.&lt;/li&gt;
&lt;li&gt;Payloads can become large because field names repeat.&lt;/li&gt;
&lt;li&gt;Parsing requires text processing.&lt;/li&gt;
&lt;li&gt;Numeric interoperability needs care.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The numeric issue is especially important for 64-bit IDs. Unity and C# can represent a value as &lt;code&gt;long&lt;/code&gt;, while JavaScript or another consumer may lose integer precision. Large account IDs, transaction IDs, or order numbers are often safer as strings across system boundaries.&lt;/p&gt;

&lt;p&gt;In Unity, loading large JSON documents at runtime also deserves measurement. Parsing several megabytes or tens of megabytes at startup and materializing the entire object graph can affect startup time, peak memory, and garbage collection.&lt;/p&gt;

&lt;p&gt;That does not make JSON a bad format. A few kilobytes of settings, a small save file, or a low-frequency API response may be completely fine. Problems appear when the data size and access pattern outgrow the original design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unity's &lt;code&gt;JsonUtility&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Unity includes &lt;code&gt;JsonUtility&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is lightweight for simple types that follow Unity's serialization rules. In suitable cases, it can allocate less than a general-purpose JSON library.&lt;/p&gt;

&lt;p&gt;However, &lt;code&gt;JsonUtility&lt;/code&gt; is not a fully general JSON document API. It works with structured JSON that maps to known C# types and inherits important Unity-serialization limitations.&lt;/p&gt;

&lt;p&gt;Notable constraints include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Dictionary&amp;lt;TKey, TValue&amp;gt;&lt;/code&gt; is not supported.&lt;/li&gt;
&lt;li&gt;Top-level arrays and primitive values are awkward and usually need wrapper classes.&lt;/li&gt;
&lt;li&gt;Arbitrary JSON trees are not its intended use case.&lt;/li&gt;
&lt;li&gt;Fine-grained naming and polymorphism support are limited.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It works well for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple configuration classes&lt;/li&gt;
&lt;li&gt;Small save DTOs&lt;/li&gt;
&lt;li&gt;Inspector-friendly data shapes&lt;/li&gt;
&lt;li&gt;Known request or response structures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For complex server responses, dictionaries, custom naming rules, or polymorphic data, Newtonsoft.Json may be a better fit.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;System.Text.Json&lt;/code&gt; can also be considered, but first verify the exact Unity version, .NET profile, IL2CPP/AOT behavior, source-generation setup, package integration, and stripping behavior. Success in the Editor under Mono does not prove that an iOS or Android IL2CPP build is production-ready.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ToJson&lt;/code&gt; still creates a JSON string, and &lt;code&gt;FromJson&lt;/code&gt; creates a new object. Large datasets still allocate and must be profiled. When updating an existing instance is appropriate, &lt;code&gt;FromJsonOverwrite&lt;/code&gt; can reduce object replacement and is worth considering.&lt;/p&gt;

&lt;h2&gt;
  
  
  MessagePack: A Runtime-Oriented Binary Format
&lt;/h2&gt;

&lt;p&gt;MessagePack represents JSON-like structures in binary form.&lt;/p&gt;

&lt;p&gt;Its main appeal is that it can be smaller than JSON and faster to serialize or deserialize, especially for numeric, array-heavy, or frequently processed data.&lt;/p&gt;

&lt;p&gt;In Unity, it becomes attractive when one of these costs is visible in profiling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON parsing&lt;/li&gt;
&lt;li&gt;Temporary string allocation&lt;/li&gt;
&lt;li&gt;Garbage collection&lt;/li&gt;
&lt;li&gt;Startup loading&lt;/li&gt;
&lt;li&gt;Network transfer&lt;/li&gt;
&lt;li&gt;Large local caches&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where MessagePack fits well
&lt;/h3&gt;

&lt;p&gt;MessagePack is most useful when machines, rather than people, own the final data.&lt;/p&gt;

&lt;p&gt;A common static-data pipeline is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Spreadsheet / YAML / CSV
        ↓ conversion and validation
MessagePack / project-specific binary
        ↓
Unity runtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Designers and engineers work with a readable source format. The game loads a validated runtime artifact.&lt;/p&gt;

&lt;p&gt;That conversion step separates authoring convenience from runtime efficiency.&lt;/p&gt;

&lt;h3&gt;
  
  
  MessagePack performance is conditional
&lt;/h3&gt;

&lt;p&gt;“MessagePack is fast” is a useful tendency, not a universal guarantee.&lt;/p&gt;

&lt;p&gt;Performance, size, and allocation depend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The selected library&lt;/li&gt;
&lt;li&gt;Resolver configuration&lt;/li&gt;
&lt;li&gt;String keys versus integer keys&lt;/li&gt;
&lt;li&gt;DTO shape&lt;/li&gt;
&lt;li&gt;Generated serialization code&lt;/li&gt;
&lt;li&gt;IL2CPP/AOT setup&lt;/li&gt;
&lt;li&gt;Compression settings&lt;/li&gt;
&lt;li&gt;The number of objects created after deserialization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MessagePack is also &lt;strong&gt;not compression&lt;/strong&gt;. If LZ4 or another compressor is added, measure both compression ratio and decompression cost with representative data.&lt;/p&gt;

&lt;p&gt;Even fast deserialization does not help much if the application then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Builds an enormous object graph&lt;/li&gt;
&lt;li&gt;Runs LINQ queries repeatedly over the data&lt;/li&gt;
&lt;li&gt;Loads content that is not needed at startup&lt;/li&gt;
&lt;li&gt;Copies results into multiple collections&lt;/li&gt;
&lt;li&gt;Uses inefficient lookup structures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If JSON is transmitted with HTTP gzip compression, its transfer size may approach MessagePack for some payloads. The JSON still has to be decompressed, parsed, and converted into objects, so profile network cost separately from CPU time and garbage collection.&lt;/p&gt;

&lt;p&gt;For some datasets, generated C# code, SQLite, FlatBuffers, MemoryPack, a custom binary, or partitioned ScriptableObjects may be more appropriate. MessagePack should earn its place through measurements and operational fit.&lt;/p&gt;

&lt;h3&gt;
  
  
  MessagePack operational and security considerations
&lt;/h3&gt;

&lt;p&gt;The biggest operational weakness is that people cannot inspect MessagePack directly.&lt;/p&gt;

&lt;p&gt;It is not suitable for hand-editing or Git diff review. During an incident, opening a binary file does not immediately explain what it contains.&lt;/p&gt;

&lt;p&gt;A production-friendly MessagePack workflow should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A debug exporter that produces JSON&lt;/li&gt;
&lt;li&gt;A decode CLI or internal inspection tool&lt;/li&gt;
&lt;li&gt;Explicit data versions&lt;/li&gt;
&lt;li&gt;Schema validation&lt;/li&gt;
&lt;li&gt;Conversion logs&lt;/li&gt;
&lt;li&gt;Source-data references&lt;/li&gt;
&lt;li&gt;Clear load-error messages&lt;/li&gt;
&lt;li&gt;Checksums or hashes for corruption detection and artifact matching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deserializing external input also makes the deserializer configuration part of the attack surface.&lt;/p&gt;

&lt;p&gt;With MessagePack for C#, consider the following for untrusted input:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;MessagePackSecurity.UntrustedData&lt;/code&gt; where appropriate.&lt;/li&gt;
&lt;li&gt;Avoid Typeless APIs and resolvers unless the use case is tightly controlled.&lt;/li&gt;
&lt;li&gt;Deserialize only approved DTO types.&lt;/li&gt;
&lt;li&gt;Apply input-size limits.&lt;/li&gt;
&lt;li&gt;Track security advisories and patch versions.&lt;/li&gt;
&lt;li&gt;Validate the resulting DTO after deserialization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A safer deserializer mode is not a substitute for validation or server-side trust boundaries.&lt;/p&gt;

&lt;p&gt;Unity IL2CPP builds add AOT constraints. As of July 2026, MessagePack for C# v3 uses source generation for AOT workflows and lists Unity &lt;code&gt;2022.3.12f1&lt;/code&gt; as its minimum supported version. Older Unity releases or older package versions require their own compatibility checks.&lt;/p&gt;

&lt;p&gt;Even with current versions, test custom resolvers, generic types, code stripping, error paths, and actual device builds early. Package requirements can change, so confirm the official documentation for the exact Unity and package versions used by the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  MessagePack is not encryption
&lt;/h3&gt;

&lt;p&gt;MessagePack is harder to read in a text editor, but it is not encrypted. Anyone who understands the format can decode it.&lt;/p&gt;

&lt;p&gt;Do not treat “binary” as tamper protection.&lt;/p&gt;

&lt;p&gt;Checksums and ordinary hashes are useful for detecting accidental corruption or confirming that generated artifacts match. They do not stop an attacker who can modify both the data and the stored hash.&lt;/p&gt;

&lt;p&gt;For malicious-tampering detection, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A keyed MAC&lt;/li&gt;
&lt;li&gt;A digital signature&lt;/li&gt;
&lt;li&gt;Server-side validation&lt;/li&gt;
&lt;li&gt;A server-authoritative design for important state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Encryption primarily provides confidentiality. It does not automatically provide complete tamper protection.&lt;/p&gt;

&lt;p&gt;The same rule applies to network traffic. MessagePack does not replace HTTPS, authentication, authorization, replay defenses, or input validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing Formats by Unity Use Case
&lt;/h2&gt;

&lt;h3&gt;
  
  
  User settings
&lt;/h3&gt;

&lt;p&gt;Audio volume, graphics quality, vibration, language, and control preferences are normally small.&lt;/p&gt;

&lt;p&gt;A reasonable default is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use PlayerPrefs for a few independent values.&lt;/li&gt;
&lt;li&gt;Use JSON when the settings have a clear structure.&lt;/li&gt;
&lt;li&gt;Prefer JSON when easy inspection is valuable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MessagePack is rarely necessary for ordinary user preferences.&lt;/p&gt;

&lt;p&gt;PlayerPrefs should remain within its natural boundary. It is not a good place for a large structured save file, secret values, or data that requires tamper resistance.&lt;/p&gt;

&lt;p&gt;Save timing also matters. Unity normally writes PlayerPrefs during application shutdown, but mobile platforms may terminate an app without a reliable quit callback. Important settings should therefore be saved at meaningful checkpoints controlled by the application, such as confirming an options screen or entering a pause state when appropriate.&lt;/p&gt;

&lt;p&gt;Do not call &lt;code&gt;PlayerPrefs.Save()&lt;/code&gt; for every slider movement. The write may cause a noticeable hitch. Save at deliberate points rather than continuously during gameplay.&lt;/p&gt;

&lt;h3&gt;
  
  
  Save data
&lt;/h3&gt;

&lt;p&gt;Save-data decisions depend heavily on game size and write frequency.&lt;/p&gt;

&lt;p&gt;For a small game, JSON is a reasonable starting point because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers can inspect it.&lt;/li&gt;
&lt;li&gt;Customer-support investigations are easier.&lt;/li&gt;
&lt;li&gt;Early schema changes are easier to understand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a large save file or frequent autosaves, MessagePack or a project-specific binary may be worth evaluating.&lt;/p&gt;

&lt;p&gt;The format is only one part of save reliability. A robust save design also needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A version number&lt;/li&gt;
&lt;li&gt;Migration from older versions&lt;/li&gt;
&lt;li&gt;Validation and normalization&lt;/li&gt;
&lt;li&gt;Writing to a temporary file first&lt;/li&gt;
&lt;li&gt;Controlled replacement of the previous file&lt;/li&gt;
&lt;li&gt;Backup and recovery behavior&lt;/li&gt;
&lt;li&gt;Corruption detection&lt;/li&gt;
&lt;li&gt;A policy for tampered data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;JSON stored as plain text can be edited easily. Even in an offline game, edited values can cause cheating, support confusion, or crashes caused by impossible state.&lt;/p&gt;

&lt;p&gt;Important state may require a keyed MAC, signature, or server validation. An ordinary hash is insufficient if the attacker can replace both the file and the hash.&lt;/p&gt;

&lt;p&gt;Whether the payload is JSON or MessagePack, add a &lt;code&gt;version&lt;/code&gt; field or equivalent from the beginning. Binary serialization does not create compatibility automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Static game data
&lt;/h3&gt;

&lt;p&gt;Static game data includes definitions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Characters&lt;/li&gt;
&lt;li&gt;Items&lt;/li&gt;
&lt;li&gt;Skills&lt;/li&gt;
&lt;li&gt;Stages&lt;/li&gt;
&lt;li&gt;Quests&lt;/li&gt;
&lt;li&gt;Shops&lt;/li&gt;
&lt;li&gt;Gacha tables&lt;/li&gt;
&lt;li&gt;Localization entries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is one of the easiest areas to outgrow a single-format design.&lt;/p&gt;

&lt;p&gt;A practical pipeline separates authoring from runtime delivery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authoring: Spreadsheet / Excel / YAML / CSV / dedicated admin UI
Conversion: Validate / normalize / generate code / export binary
Runtime: MessagePack / project-specific binary / generated C# data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Choose the authoring source based on the people and workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Designers editing large tables: Spreadsheet or Excel&lt;/li&gt;
&lt;li&gt;Engineers maintaining structured configuration: YAML&lt;/li&gt;
&lt;li&gt;Simple interoperable tables: CSV&lt;/li&gt;
&lt;li&gt;Live operations teams: Dedicated browser-based tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choose the runtime format based on loading cost, memory behavior, validation guarantees, and update strategy.&lt;/p&gt;

&lt;p&gt;Small datasets may remain in JSON without issue. If growth is expected, building the conversion and validation pipeline early is often more valuable than prematurely optimizing the final serializer.&lt;/p&gt;

&lt;p&gt;A “project-specific binary” should mean a documented, validated runtime artifact with versioning, migration rules, debug export, and generation tooling. It should not merely mean “a file that is difficult to read.”&lt;/p&gt;

&lt;h3&gt;
  
  
  ScriptableObject and external formats
&lt;/h3&gt;

&lt;p&gt;ScriptableObject is excellent for Unity-owned data that benefits from Inspector editing and asset references.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enemy AI tuning&lt;/li&gt;
&lt;li&gt;Effect references&lt;/li&gt;
&lt;li&gt;Prefab references&lt;/li&gt;
&lt;li&gt;Camera settings&lt;/li&gt;
&lt;li&gt;Small gameplay configuration assets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is less convenient as the only solution for huge tabular datasets, data shared with a server, or data that needs spreadsheet-style editing and cross-table validation.&lt;/p&gt;

&lt;p&gt;A useful division of responsibility is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ScriptableObject for Unity asset-oriented configuration&lt;/li&gt;
&lt;li&gt;Spreadsheet, YAML, or CSV for authoring external static data&lt;/li&gt;
&lt;li&gt;MessagePack or another binary for runtime distribution&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Localization data
&lt;/h3&gt;

&lt;p&gt;Localization datasets grow quickly and are often exchanged with translators.&lt;/p&gt;

&lt;p&gt;Spreadsheet or CSV is usually convenient for authoring and vendor exchange. At runtime, partition by language or feature and convert the source into a structure that supports efficient ID lookup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Addressables and AssetBundle metadata
&lt;/h3&gt;

&lt;p&gt;Human-reviewed Addressables labels, download groups, or build settings may fit YAML. Generated metadata and tool-to-tool interchange often fit JSON.&lt;/p&gt;

&lt;p&gt;There is usually no need to parse YAML on every player startup. Data known at build time should be validated and converted into a stable runtime representation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a Format for Server APIs
&lt;/h2&gt;

&lt;p&gt;Server communication is where the JSON-versus-MessagePack discussion becomes most visible.&lt;/p&gt;

&lt;p&gt;The decision should include incident response, customer support, monitoring, and deployment—not only payload size.&lt;/p&gt;

&lt;h3&gt;
  
  
  APIs that usually fit JSON
&lt;/h3&gt;

&lt;p&gt;JSON is a good default for APIs such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login&lt;/li&gt;
&lt;li&gt;User-profile retrieval&lt;/li&gt;
&lt;li&gt;News and announcement retrieval&lt;/li&gt;
&lt;li&gt;Shop information&lt;/li&gt;
&lt;li&gt;Static-data version checks&lt;/li&gt;
&lt;li&gt;Admin-panel APIs&lt;/li&gt;
&lt;li&gt;External-service integration&lt;/li&gt;
&lt;li&gt;Diagnostic APIs&lt;/li&gt;
&lt;li&gt;Low-frequency operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For these endpoints, observability and compatibility often matter more than a small payload reduction.&lt;/p&gt;

&lt;p&gt;JSON works naturally with &lt;code&gt;curl&lt;/code&gt;, Postman, server logs, dashboards, and proxy tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  APIs that may benefit from MessagePack
&lt;/h3&gt;

&lt;p&gt;MessagePack becomes more attractive for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Very frequently called endpoints&lt;/li&gt;
&lt;li&gt;Large list responses&lt;/li&gt;
&lt;li&gt;Real-time or near-real-time traffic&lt;/li&gt;
&lt;li&gt;Battle logs or state synchronization&lt;/li&gt;
&lt;li&gt;Mobile traffic where transfer volume matters&lt;/li&gt;
&lt;li&gt;Endpoints where JSON parsing is a measured bottleneck&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adopting MessagePack should come with operational support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A JSON decoder for investigations&lt;/li&gt;
&lt;li&gt;API and data versioning&lt;/li&gt;
&lt;li&gt;Shared DTO rules between client and server&lt;/li&gt;
&lt;li&gt;Backward-compatibility policy&lt;/li&gt;
&lt;li&gt;Readable server-side diagnostic logging&lt;/li&gt;
&lt;li&gt;Request and response validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using MessagePack for every endpoint often adds more operational cost than it saves. Limiting it to measured hot paths is usually easier to maintain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using JSON in development and MessagePack in production
&lt;/h3&gt;

&lt;p&gt;A development-versus-production switch can be useful, but it has a dangerous failure mode: testing only JSON in development while production alone uses MessagePack.&lt;/p&gt;

&lt;p&gt;Avoid that design.&lt;/p&gt;

&lt;p&gt;Instead, separate the &lt;strong&gt;wire format&lt;/strong&gt; from the &lt;strong&gt;debug representation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A practical setup is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Local development: Switch between JSON and MessagePack
CI and staging: Test both, and always exercise the production path
Production: Use MessagePack only for endpoints where it provides value
Investigation: Render decoded DTOs as masked JSON
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the following common across both formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DTO meaning&lt;/li&gt;
&lt;li&gt;Data version&lt;/li&gt;
&lt;li&gt;Normalization&lt;/li&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Change only the encoder and decoder based on &lt;code&gt;Content-Type&lt;/code&gt;, &lt;code&gt;Accept&lt;/code&gt;, or explicit configuration.&lt;/p&gt;

&lt;p&gt;Automated tests can serialize the same DTO through JSON and MessagePack, deserialize both, and compare the normalized results. Once MessagePack is selected for production, engineers should exercise that path continuously rather than only before release.&lt;/p&gt;

&lt;p&gt;This is especially important for IL2CPP/AOT behavior, resolver configuration, integer-key compatibility, compression, and client/server implementation differences.&lt;/p&gt;

&lt;h3&gt;
  
  
  When JSON + gzip remains the right production choice
&lt;/h3&gt;

&lt;p&gt;Production does not automatically mean MessagePack.&lt;/p&gt;

&lt;p&gt;If transfer size is the main problem while JSON parsing and allocation remain acceptable, JSON + gzip is a strong tradeoff.&lt;/p&gt;

&lt;p&gt;Repeated property names and recurring text usually compress well. The project keeps the compatibility and investigation benefits of JSON while reducing bandwidth.&lt;/p&gt;

&lt;p&gt;In HTTP terms, the media type and compression are separate concerns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content-Type: application/json
Content-Encoding: gzip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small responses may not benefit because headers and compression work become significant relative to the payload. Use representative data to choose a minimum size threshold rather than compressing every response blindly.&lt;/p&gt;

&lt;p&gt;If request bodies are also compressed, client and server support must be explicitly aligned.&lt;/p&gt;

&lt;p&gt;gzip is compression, not encryption. It may prevent a raw byte dump from being immediately readable as plain JSON, but identifying and decompressing it is straightforward. Treat that as incidental obfuscation at most, not as a security boundary.&lt;/p&gt;

&lt;p&gt;Use HTTPS/TLS for transport confidentiality and integrity. Use authentication, authorization, replay defenses where needed, and server-side validation for game rules.&lt;/p&gt;

&lt;p&gt;A useful decision split is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only transfer size is a problem: evaluate JSON + gzip.&lt;/li&gt;
&lt;li&gt;JSON parsing and GC are also a problem: evaluate MessagePack.&lt;/li&gt;
&lt;li&gt;Neither is a measured problem: keep plain JSON.&lt;/li&gt;
&lt;li&gt;The payload is a large binary asset: avoid Base64 inside JSON and use file delivery or a separate endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Mixing JSON and MessagePack is acceptable
&lt;/h3&gt;

&lt;p&gt;A project does not need one wire format for every API.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Normal APIs and admin tools: JSON
Large or latency-sensitive endpoints: MessagePack
Logs and audit events: JSON Lines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even when the encoding differs, preserve common DTO semantics, versions, validation rules, and monitoring conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  DTO Design Matters More Than the Serializer
&lt;/h2&gt;

&lt;p&gt;A poor DTO remains difficult to maintain in every format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate Unity runtime models from DTOs
&lt;/h3&gt;

&lt;p&gt;Do not serialize a Unity gameplay object directly just because it already contains the values you need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Player&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;AvatarPrefab&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;RuntimeAnimatorController&lt;/span&gt; &lt;span class="n"&gt;Animator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This class mixes data with Unity asset references and runtime state.&lt;/p&gt;

&lt;p&gt;Create a dedicated DTO instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Serializable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerDto&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;avatarId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lowercase field names above intentionally produce &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;level&lt;/code&gt;, and &lt;code&gt;avatarId&lt;/code&gt; with &lt;code&gt;JsonUtility&lt;/code&gt;. If the project uses PascalCase C# members, configure property naming through the selected JSON library instead.&lt;/p&gt;

&lt;p&gt;DTOs should primarily contain values such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;int&lt;/code&gt;, &lt;code&gt;long&lt;/code&gt;, &lt;code&gt;float&lt;/code&gt;, and &lt;code&gt;bool&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Strings&lt;/li&gt;
&lt;li&gt;Arrays and &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;IDs that reference assets or definitions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid putting these directly into persistence or network DTOs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GameObject&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Texture&lt;/code&gt; or &lt;code&gt;Sprite&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ScriptableObject&lt;/code&gt; references&lt;/li&gt;
&lt;li&gt;Events&lt;/li&gt;
&lt;li&gt;Runtime caches&lt;/li&gt;
&lt;li&gt;UI state&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Define field and null policies
&lt;/h3&gt;

&lt;p&gt;With JSON and string-key MessagePack, field names become part of the serialized contract. With integer-key MessagePack, the key numbers become the contract.&lt;/p&gt;

&lt;p&gt;Treat published fields like API fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not casually rename them.&lt;/li&gt;
&lt;li&gt;Avoid removing them without migration.&lt;/li&gt;
&lt;li&gt;Prefer adding new optional fields.&lt;/li&gt;
&lt;li&gt;Normalize older data during loading.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also define whether missing collections become &lt;code&gt;null&lt;/code&gt; or empty collections.&lt;/p&gt;

&lt;p&gt;A normalization step reduces repeated null checks in gameplay code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InventoryDto&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;itemIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Normalize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;itemIds&lt;/span&gt; &lt;span class="p"&gt;??=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validation should still reject impossible ranges, unknown enum values, oversized strings, and invalid IDs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manage MessagePack integer keys deliberately
&lt;/h3&gt;

&lt;p&gt;With MessagePack for C#, integer keys may improve compactness and speed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MessagePackObject&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CharacterDto&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They also create a compatibility obligation.&lt;/p&gt;

&lt;p&gt;A safe policy is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never change an existing key number.&lt;/li&gt;
&lt;li&gt;Never reuse a deleted key number.&lt;/li&gt;
&lt;li&gt;Append new fields at the end.&lt;/li&gt;
&lt;li&gt;Give the data an explicit version where migration is required.&lt;/li&gt;
&lt;li&gt;Test old payloads against new readers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;String keys favor readability and evolution. Integer keys favor compactness and throughput. Choose based on data lifetime and compatibility needs, not only benchmark numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Failure Modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Failure 1: Using the authoring format directly at runtime
&lt;/h3&gt;

&lt;p&gt;Reading designer-owned YAML or JSON directly in the player is convenient at first.&lt;/p&gt;

&lt;p&gt;As the dataset grows, loading, allocation, validation, and error reporting become harder. Convert the source into a normalized runtime representation, remove comments and unused columns, validate references, and emit only the values the game needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 2: Assuming JSON is safe because it is standard
&lt;/h3&gt;

&lt;p&gt;External JSON and user-editable JSON are untrusted input.&lt;/p&gt;

&lt;p&gt;Prepare for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing required fields&lt;/li&gt;
&lt;li&gt;Wrong types&lt;/li&gt;
&lt;li&gt;Out-of-range numbers&lt;/li&gt;
&lt;li&gt;Huge strings or arrays&lt;/li&gt;
&lt;li&gt;Unknown enum values&lt;/li&gt;
&lt;li&gt;Invalid IDs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;JSON and MessagePack both require validation after deserialization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 3: Expecting MessagePack to make the whole load fast
&lt;/h3&gt;

&lt;p&gt;MessagePack can reduce representation size and parse cost. It cannot fix inefficient data partitioning, repeated searches, unnecessary copies, or excessive object construction.&lt;/p&gt;

&lt;p&gt;Measure the complete loading path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 4: Making binary data impossible to investigate
&lt;/h3&gt;

&lt;p&gt;Binary formats reduce observability unless the team deliberately restores it.&lt;/p&gt;

&lt;p&gt;Record enough metadata to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which source produced this file?&lt;/li&gt;
&lt;li&gt;Which converter version was used?&lt;/li&gt;
&lt;li&gt;When was it generated?&lt;/li&gt;
&lt;li&gt;Which data version does it contain?&lt;/li&gt;
&lt;li&gt;How many records were loaded?&lt;/li&gt;
&lt;li&gt;Can it be exported back to readable JSON?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without those answers, a compact file can become an expensive operational problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Project Structures
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Small project
&lt;/h3&gt;

&lt;p&gt;Keep the system simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User settings: PlayerPrefs / JSON
Save data: JSON
Static game data: JSON / ScriptableObject
Server APIs: JSON
Tool configuration: YAML
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For small datasets, MessagePack's operational cost may exceed its benefit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Medium project
&lt;/h3&gt;

&lt;p&gt;Separate source data from runtime data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User settings: JSON
Save data: JSON, with MessagePack evaluated if needed
Static-data source: Spreadsheet / YAML / CSV
Static-data runtime: MessagePack / project-specific binary
Server APIs: JSON
Large API payloads or production hot paths: JSON + gzip / MessagePack
Logs: JSON Lines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run validation before the build and reject duplicate IDs, missing references, invalid ranges, and unsupported values.&lt;/p&gt;

&lt;h3&gt;
  
  
  Large or long-running project
&lt;/h3&gt;

&lt;p&gt;Design the pipeline rather than choosing one serializer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authoring: Spreadsheet / Excel / YAML / dedicated admin UI
Conversion: Validate / normalize / generate code / export binary
Unity runtime: MessagePack / project-specific binary / generated loader
Server: JSON APIs / gzip compression / MessagePack hot paths
Operations: JSON logs / decode tools / data-version tracking
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make ownership explicit: where conversion happens, where validation happens, and how incidents are investigated.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Decision Flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Do people edit the data directly?
   Yes → YAML / Spreadsheet / CSV / dedicated UI
   No  → Continue

2. Must it integrate with external services or admin tools?
   Yes → Start with JSON
   No  → Continue

3. Is network size or frequency a measured problem?
   Yes → Evaluate JSON + gzip, MessagePack, or API partitioning
   No  → Continue

4. Does the Unity runtime load a large local dataset?
   Yes → Evaluate MessagePack, generated code, partitioning, or another binary
   No  → JSON or ScriptableObject may be enough

5. Must people inspect the data during an incident?
   Yes → Keep JSON in the workflow or provide a decoder
   No  → A binary-oriented representation may be acceptable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not try to predict one perfect format at the beginning of the project. Change formats at clear boundaries and optimize only the parts that need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;YAML, JSON, and MessagePack solve different problems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;YAML is strong for hand-written configuration, comments, and reviewable source data.&lt;/li&gt;
&lt;li&gt;JSON is strong for interoperability, observability, APIs, tools, and small datasets.&lt;/li&gt;
&lt;li&gt;MessagePack is strong for validated runtime data and measured hot paths where size, parsing, or allocation matters.&lt;/li&gt;
&lt;li&gt;JSON + gzip is a legitimate production option when bandwidth is the main issue and JSON parsing remains acceptable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of them is universal.&lt;/p&gt;

&lt;p&gt;If MessagePack is introduced, introduce the supporting tools as well: decoders, versioning, validation, device testing, and representative benchmarks.&lt;/p&gt;

&lt;p&gt;The durable rule is still this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Use human-friendly formats at authoring time, machine-friendly formats at runtime, and choose network formats by balancing observability against efficiency.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8259" rel="noopener noreferrer"&gt;RFC 8259 — The JavaScript Object Notation Data Interchange Format&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://yaml.org/spec/1.2.2/" rel="noopener noreferrer"&gt;YAML 1.2.2 Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/aaubry/YamlDotNet" rel="noopener noreferrer"&gt;YamlDotNet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://msgpack.org/" rel="noopener noreferrer"&gt;MessagePack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/MessagePack-CSharp/MessagePack-CSharp" rel="noopener noreferrer"&gt;MessagePack for C#&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110" rel="noopener noreferrer"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8446" rel="noopener noreferrer"&gt;RFC 8446 — TLS 1.3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/ScriptReference/JsonUtility.ToJson.html" rel="noopener noreferrer"&gt;Unity 6.4 — JsonUtility.ToJson&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/ScriptReference/JsonUtility.FromJson.html" rel="noopener noreferrer"&gt;Unity 6.4 — JsonUtility.FromJson&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/ScriptReference/JsonUtility.FromJsonOverwrite.html" rel="noopener noreferrer"&gt;Unity 6.4 — JsonUtility.FromJsonOverwrite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/ScriptReference/PlayerPrefs.html" rel="noopener noreferrer"&gt;Unity 6.4 — PlayerPrefs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/ScriptReference/PlayerPrefs.Save.html" rel="noopener noreferrer"&gt;Unity 6.4 — PlayerPrefs.Save&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/ScriptReference/MonoBehaviour.OnApplicationQuit.html" rel="noopener noreferrer"&gt;Unity 6.4 — MonoBehaviour.OnApplicationQuit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>json</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>What MasterMemory Solves—and What It Doesn't: A Practical Guide to Static Game Data in Unity</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Wed, 15 Jul 2026 03:52:57 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/what-mastermemory-solves-and-what-it-doesnt-a-practical-guide-to-static-game-data-in-unity-dn1</link>
      <guid>https://dev.to/gamedevtoollab/what-mastermemory-solves-and-what-it-doesnt-a-practical-guide-to-static-game-data-in-unity-dn1</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When you build games with Unity, you eventually run into the problem of managing static game data—often called &lt;em&gt;master data&lt;/em&gt; in Japanese game development.&lt;/p&gt;

&lt;p&gt;At first, ScriptableObject may be more than enough. If your project has a few dozen items, a few dozen enemies, and only a small number of stage definitions, ScriptableObject is convenient because you can inspect and edit everything directly in the Unity Editor.&lt;/p&gt;

&lt;p&gt;As the project grows, however, the situation changes.&lt;/p&gt;

&lt;p&gt;You may end up with tables for items, characters, skills, quests, rewards, shops, gacha pools, stages, enemy placements, progression curves, and localization text. The data is no longer edited only by programmers. Planners and game designers may need to work with it in Excel or Google Sheets.&lt;/p&gt;

&lt;p&gt;At that point, the problem is no longer just choosing a file format. You need to think about questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do you load a large amount of data quickly?&lt;/li&gt;
&lt;li&gt;How do you write ID lookups and composite-key queries safely?&lt;/li&gt;
&lt;li&gt;Should CSV or JSON be parsed directly at runtime?&lt;/li&gt;
&lt;li&gt;Is it reasonable to create a large number of Dictionaries?&lt;/li&gt;
&lt;li&gt;How do you validate references between tables?&lt;/li&gt;
&lt;li&gt;How do you debug data after converting it to binary?&lt;/li&gt;
&lt;li&gt;How do you connect the source data edited by planners to the data loaded by Unity?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the runtime loading and lookup part of that problem, one strong option is Cysharp's &lt;a href="https://github.com/Cysharp/MasterMemory" rel="noopener noreferrer"&gt;MasterMemory&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The official README describes MasterMemory as a “Source Generator based Embedded Typed Readonly In-Memory Document Database” for .NET and Unity. In practical terms, you define your schema as C# types, a Source Generator creates a typed read-only in-memory database API, and the application loads MessagePack binary data that can be queried through type-safe methods.&lt;/p&gt;

&lt;p&gt;The official README highlights performance compared with SQLite, low allocation during queries, a small database size, and generated database structures that are type-safe and IDE-friendly.&lt;/p&gt;

&lt;p&gt;Cygames Engineers' Blog also has useful articles about the design philosophy behind MasterMemory and the Validator introduced in v2. These articles are in Japanese, but they are valuable references:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tech.cygames.co.jp/archives/3269/" rel="noopener noreferrer"&gt;MasterMemory: A Read-Only In-Memory Database for Unity and .NET Core&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tech.cygames.co.jp/archives/3367/" rel="noopener noreferrer"&gt;MasterMemory v2 for Parameter Validation in Master Data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also clear Japanese articles covering Unity setup and basic usage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://zenn.dev/takodevlog/articles/9a66226d3f7a72" rel="noopener noreferrer"&gt;Introducing MasterMemory for Master Data Management in Unity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zenn.dev/clay_andromeda/articles/mini-master-memory" rel="noopener noreferrer"&gt;Learning Efficient Game Master Data Management from MasterMemory&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing to keep in mind is that information about MasterMemory v2 and earlier often assumes the old Code Generator workflow, while v3 and later are based on Source Generators. Always check the README and release notes for the version you actually plan to use.&lt;/p&gt;

&lt;p&gt;Unless otherwise noted, the API behavior described in this article refers to MasterMemory v3.0.4.&lt;/p&gt;

&lt;p&gt;This article is not a step-by-step installation tutorial. Instead, it focuses on what MasterMemory solves in a production Unity project and where its responsibilities end.&lt;/p&gt;

&lt;p&gt;The conclusion is straightforward: MasterMemory is a strong option when you need to handle large amounts of read-only static game data with fast loading, low memory overhead, and type-safe queries. It does not, however, decide how planners edit the source data, how localization is managed, how diffs are reviewed, how CI generates the database, or how environment-specific data is deployed.&lt;/p&gt;

&lt;p&gt;The most useful way to evaluate it is as a &lt;strong&gt;fast, type-safe, read-only database&lt;/strong&gt;, separate from the pipeline around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Basic Role of MasterMemory
&lt;/h2&gt;

&lt;p&gt;Three characteristics are central to understanding MasterMemory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read-only&lt;/li&gt;
&lt;li&gt;In-memory&lt;/li&gt;
&lt;li&gt;Strongly typed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination is a good fit for static game data.&lt;/p&gt;

&lt;p&gt;Most game master data is not supposed to be mutated continuously during gameplay.&lt;/p&gt;

&lt;p&gt;A live-service game may download a new data package from a server or switch to a different dataset for an event. But once the client has loaded a dataset, it is usually treated as immutable for the rest of the session.&lt;/p&gt;

&lt;p&gt;If item definitions or skill definitions can be modified directly during gameplay, several problems become more likely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It becomes difficult to know when a value changed.&lt;/li&gt;
&lt;li&gt;It becomes difficult to identify which system changed it.&lt;/li&gt;
&lt;li&gt;Cached data can become inconsistent.&lt;/li&gt;
&lt;li&gt;Threading and asynchronous processing become harder to reason about.&lt;/li&gt;
&lt;li&gt;Bugs become less reproducible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treating master data as immutable after loading simplifies the runtime architecture.&lt;/p&gt;

&lt;p&gt;MasterMemory is designed around that assumption.&lt;/p&gt;

&lt;p&gt;“Read-only” does not mean that a running game can never switch to new master data. It means that the active &lt;code&gt;MemoryDatabase&lt;/code&gt; should be treated as an immutable snapshot. When new data is needed, you can construct a new database from a new binary package, or use &lt;code&gt;ToImmutableBuilder()&lt;/code&gt; to apply changes and build another immutable database, then replace the shared reference.&lt;/p&gt;

&lt;p&gt;The data is converted to binary ahead of time, loaded into a &lt;code&gt;MemoryDatabase&lt;/code&gt; at startup or another appropriate point, and queried through generated table APIs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MemoryDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ItemMasterTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindByItemId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The examples in this article use &lt;code&gt;FindBy...&lt;/code&gt;, but production code must also account for missing keys.&lt;/p&gt;

&lt;p&gt;In the v3.0.4 README, some older wording still appears in one place, but the &lt;code&gt;MemoryDatabase&lt;/code&gt; / &lt;code&gt;RangeView&lt;/code&gt; section and the Generator Option documentation describe the current behavior: for a unique key, &lt;code&gt;FindBy...&lt;/code&gt; throws &lt;code&gt;KeyNotFoundException&lt;/code&gt; when the key is missing. A generated &lt;code&gt;TryFindBy...&lt;/code&gt; method is also available.&lt;/p&gt;

&lt;p&gt;For IDs originating outside the validated master-data package—such as save data, server responses, or QA commands—using &lt;code&gt;TryFindBy...&lt;/code&gt; is usually the safest default.&lt;/p&gt;

&lt;p&gt;If the Generator Option &lt;code&gt;IsReturnNullIfKeyNotFound&lt;/code&gt; is enabled, unique-key lookups can return &lt;code&gt;null&lt;/code&gt; instead. A &lt;code&gt;FindBy...&lt;/code&gt; query on a &lt;code&gt;NonUnique&lt;/code&gt; key returns a &lt;code&gt;RangeView&amp;lt;T&amp;gt;&lt;/code&gt;, and an unmatched query produces an empty result.&lt;/p&gt;

&lt;p&gt;A useful division of responsibility is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detect invalid references inside the master-data package with Validator.&lt;/li&gt;
&lt;li&gt;Defend against invalid external IDs at runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once this structure is in place, runtime access becomes much simpler.&lt;/p&gt;

&lt;p&gt;You no longer need to parse CSV at startup. You can reduce the amount of JSON-to-Dictionary conversion code. You also need fewer hand-written lookup Dictionaries for every table.&lt;/p&gt;

&lt;p&gt;ScriptableObject, SQLite, or JSON loaded through Addressables may still be the right choice in some projects. But for a large set of read-heavy tabular data, MasterMemory's design maps well to common game requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MasterMemory Solves
&lt;/h2&gt;

&lt;p&gt;MasterMemory is particularly effective in the following areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Storing master data in a read-only in-memory database&lt;/li&gt;
&lt;li&gt;Generating type-safe query APIs with a Source Generator&lt;/li&gt;
&lt;li&gt;Supporting PrimaryKey and SecondaryKey lookups&lt;/li&gt;
&lt;li&gt;Supporting composite keys, range queries, and closest-value queries&lt;/li&gt;
&lt;li&gt;Loading MessagePack binary data&lt;/li&gt;
&lt;li&gt;Interning repeated strings to share references&lt;/li&gt;
&lt;li&gt;Validating data consistency through Validator&lt;/li&gt;
&lt;li&gt;Exposing Metadata that can support surrounding tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Existing articles often focus on setup, raw performance, or comparisons with Dictionary.&lt;/p&gt;

&lt;p&gt;The official README includes claims such as &lt;code&gt;4700 times faster than SQLite&lt;/code&gt;, &lt;code&gt;zero allocation per query&lt;/code&gt;, and &lt;code&gt;DB size is small&lt;/code&gt;. Those numbers come from specific benchmark conditions and example datasets. The actual difference in your project depends on the schema, record count, query frequency, load timing, and target platform.&lt;/p&gt;

&lt;p&gt;In Unity, perceived performance can also change depending on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Editor versus device&lt;/li&gt;
&lt;li&gt;Mono versus IL2CPP&lt;/li&gt;
&lt;li&gt;Whether the binary is loaded from Resources, StreamingAssets, Addressables, an AssetBundle, or a remote server&lt;/li&gt;
&lt;li&gt;The I/O cost of retrieving the binary&lt;/li&gt;
&lt;li&gt;Whether the caller applies LINQ or copies results into another collection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;zero allocation per query&lt;/code&gt; should be understood as a property of the query API itself. If the calling code adds LINQ chains or copies the result into a new &lt;code&gt;List&lt;/code&gt;, those operations may allocate separately.&lt;/p&gt;

&lt;p&gt;Even with those caveats, the underlying direction is well suited to static game data: build sorted structures ahead of use, rely on binary search, and avoid unnecessary runtime work.&lt;/p&gt;

&lt;p&gt;The Cygames article explains that MasterMemory uses sorted arrays and binary search to achieve lookup performance close to Dictionary while improving construction cost and memory efficiency in suitable workloads.&lt;/p&gt;

&lt;p&gt;The following sections focus on the production benefits that become important beyond a simple benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read-Only Is a Strength, Not Just a Restriction
&lt;/h2&gt;

&lt;p&gt;MasterMemory is read-only.&lt;/p&gt;

&lt;p&gt;That can sound like a limitation, but for game master data it is often a major advantage. Master data usually describes definitions used to interpret the mutable state of the game.&lt;/p&gt;

&lt;p&gt;For example, the number of items owned by a player belongs to save data and changes over time.&lt;/p&gt;

&lt;p&gt;The definition that item ID 1001 is a herb with a price of 50 and a healing value of 100 is master data. That definition should normally remain stable while the game is running.&lt;/p&gt;

&lt;p&gt;Mixing those two categories makes the architecture harder to maintain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Mutable state&lt;/span&gt;
&lt;span class="n"&gt;playerInventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Immutable definition&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ItemMasterTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindByItemId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treating master data as read-only provides several benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better reproducibility&lt;/li&gt;
&lt;li&gt;Less rebuilding of lookup caches after data changes&lt;/li&gt;
&lt;li&gt;Simpler code on the consumer side&lt;/li&gt;
&lt;li&gt;More stable behavior after asynchronous initialization&lt;/li&gt;
&lt;li&gt;Easier tests with deterministic inputs&lt;/li&gt;
&lt;li&gt;Protection against accidental runtime mutation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same master data may be consumed by battle logic, UI, shops, rewards, tutorials, localization, and visual presentation. If any of those systems can mutate the shared definitions, assumptions made by the other systems can collapse.&lt;/p&gt;

&lt;p&gt;Read-only data narrows runtime responsibilities and makes those shared assumptions safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repeated Strings Can Be Shared Automatically
&lt;/h2&gt;

&lt;p&gt;By default, MasterMemory interns identical strings while constructing the database.&lt;/p&gt;

&lt;p&gt;Static game data often repeats category names, localization keys, asset identifiers, and other strings across many rows. Without interning, identical text may exist as multiple string instances. With interning, those rows can share the same reference.&lt;/p&gt;

&lt;p&gt;This can be useful for denormalized tables where strings are repeated to make queries or exports simpler. The behavior can be disabled through the &lt;code&gt;internString&lt;/code&gt; argument of &lt;code&gt;MemoryDatabase&lt;/code&gt;, so it can be evaluated against the actual characteristics of the data and measured on target devices.&lt;/p&gt;

&lt;p&gt;It is less visible than query speed, but for a game that keeps large tables resident in memory, it is a practical benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type-Safe Query APIs Improve Maintainability
&lt;/h2&gt;

&lt;p&gt;One of MasterMemory's strongest features is the type-safe query API generated by its Source Generator.&lt;/p&gt;

&lt;p&gt;The official README explains that the C# schema produces a typed database structure with IDE completion.&lt;/p&gt;

&lt;p&gt;For a small project, loading CSV into a Dictionary may be enough.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"ItemId"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a project grows, string-based access becomes fragile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Column-name typos are not detected until runtime.&lt;/li&gt;
&lt;li&gt;Renaming a column is difficult to propagate safely.&lt;/li&gt;
&lt;li&gt;Refactoring support is weak.&lt;/li&gt;
&lt;li&gt;Type conversion logic spreads across the codebase.&lt;/li&gt;
&lt;li&gt;It becomes unclear which columns are valid lookup keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With MasterMemory, &lt;code&gt;PrimaryKey&lt;/code&gt; and &lt;code&gt;SecondaryKey&lt;/code&gt; attributes define the indexes, and query methods are generated from the schema.&lt;/p&gt;

&lt;p&gt;The following is a conceptual example. Follow the recommended definitions for the versions of MasterMemory and MessagePack used by your project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;MemoryTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;MessagePackObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ItemMaster&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PrimaryKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;ItemId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;SecondaryKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;NonUnique&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ItemType&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;NameKey&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this article, the generated table API for &lt;code&gt;ItemMaster&lt;/code&gt; is consistently written as &lt;code&gt;ItemMasterTable&lt;/code&gt;. The string in &lt;code&gt;[MemoryTable("item")]&lt;/code&gt; identifies the table in the database binary; it serves a different purpose from the generated C# type and property names.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ItemMasterTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindByItemId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;weapons&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ItemMasterTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindByType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ItemType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Weapon&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The benefit is not merely shorter code.&lt;/p&gt;

&lt;p&gt;The available lookup paths become explicit in the API. IDE completion shows the generated methods, renames are easier to track, and incorrect types are detected at compile time.&lt;/p&gt;

&lt;p&gt;Master data is rarely maintained by only its original author. Another programmer may inherit it years later. Large balance changes may arrive near the end of production. In a live-service game, new tables and rows continue to accumulate.&lt;/p&gt;

&lt;p&gt;If access logic is scattered across string keys and custom Dictionaries, the system becomes difficult to understand. A generated typed API preserves the intent of each lookup in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Composite Keys, Range Queries, and Closest-Value Queries Fit Game Data
&lt;/h2&gt;

&lt;p&gt;Game master data often needs more than a simple ID lookup.&lt;/p&gt;

&lt;p&gt;ID queries are still the foundation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ItemMasterTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindByItemId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But production games regularly need queries such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrieve a reward by stage ID and difficulty.&lt;/li&gt;
&lt;li&gt;Retrieve progression values by character ID and level.&lt;/li&gt;
&lt;li&gt;Retrieve products by shop ID and category.&lt;/li&gt;
&lt;li&gt;Retrieve rewards for a rank.&lt;/li&gt;
&lt;li&gt;Find the rating definition closest to a score.&lt;/li&gt;
&lt;li&gt;Retrieve active rows for an event ID.&lt;/li&gt;
&lt;li&gt;Retrieve experience values within a level range.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Dictionary-only implementation tends to accumulate supporting structures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ItemMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;itemById&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ItemType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ItemMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;itemsByType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;stageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Difficulty&lt;/span&gt; &lt;span class="n"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;StageRewardMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rewardByStageAndDifficulty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LevelMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;levelsByCharacterId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is nothing inherently wrong with that approach. For a small project, a few custom Dictionaries may be the clearest solution.&lt;/p&gt;

&lt;p&gt;As the table count, lookup patterns, and team size grow, however, common problems appear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nobody is sure which Dictionary is the canonical lookup path.&lt;/li&gt;
&lt;li&gt;Multiple Dictionaries are created for the same purpose.&lt;/li&gt;
&lt;li&gt;Initialization order becomes complicated.&lt;/li&gt;
&lt;li&gt;Memory usage becomes difficult to understand.&lt;/li&gt;
&lt;li&gt;Rebuild logic is missed when data changes.&lt;/li&gt;
&lt;li&gt;Composite-key conventions differ across the project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MasterMemory can generate unique and non-unique lookups, composite-key queries, range queries, and closest-value queries from &lt;code&gt;PrimaryKey&lt;/code&gt; and &lt;code&gt;SecondaryKey&lt;/code&gt; definitions.&lt;/p&gt;

&lt;p&gt;The official README includes examples such as multi-key queries, &lt;code&gt;FindClosestBy***&lt;/code&gt;, and &lt;code&gt;FindRangeBy***&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This moves the decision about how a table should be queried closer to the schema itself. Lookup structures are less likely to spread throughout consumer code.&lt;/p&gt;

&lt;p&gt;In a multi-programmer project, consistency has value by itself. If the team knows that a particular piece of data always comes from a particular generated table method, reviews become easier and unnecessary caches are less likely to proliferate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Value of Reducing Hand-Written Dictionaries
&lt;/h2&gt;

&lt;p&gt;Discussions about MasterMemory often turn into a simple question: “Is it faster than Dictionary?”&lt;/p&gt;

&lt;p&gt;Performance matters, but production value also comes from leaving behind a workflow where every new query requires another custom Dictionary.&lt;/p&gt;

&lt;p&gt;A data repository that starts small can gradually turn into something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MasterDataRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ItemMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;itemById&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SkillMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;skillById&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CharacterMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;characterById&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SkillMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;skillsByCharacterId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;stageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;StageMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stageByKey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShopItemMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;shopItemsByShopId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This repository may initially be convenient. Over time, it often develops familiar problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initialization grows longer.&lt;/li&gt;
&lt;li&gt;Dictionary construction code accumulates.&lt;/li&gt;
&lt;li&gt;It becomes unclear which caches are actually required.&lt;/li&gt;
&lt;li&gt;Unused caches remain in memory.&lt;/li&gt;
&lt;li&gt;Every new query pattern adds another structure.&lt;/li&gt;
&lt;li&gt;Tests become harder to write.&lt;/li&gt;
&lt;li&gt;Validation logic becomes mixed with cache construction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, the data repository itself has become technical debt.&lt;/p&gt;

&lt;p&gt;MasterMemory moves many of these basic lookup structures into the schema and generated code.&lt;/p&gt;

&lt;p&gt;Project-specific convenience methods and cross-table queries will still be necessary. But the team no longer has to reimplement ID lookup, composite-key lookup, range lookup, and non-unique grouping every time.&lt;/p&gt;

&lt;p&gt;That improves not only performance characteristics, but also architectural clarity.&lt;/p&gt;

&lt;p&gt;Dictionary is still the better choice in some cases. If there are only a few tables, every query is a simple ID lookup, and the data is temporary or frequently replaced during runtime, a straightforward Dictionary may be easier to manage. The same may be true for editor tooling or debugging data that developers want to inspect and modify directly.&lt;/p&gt;

&lt;p&gt;The goal is not to reject Dictionary. MasterMemory becomes valuable when lookup patterns and cache-construction code begin to scale faster than the team can manage comfortably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Binary Data Simplifies the Runtime Loading Path
&lt;/h2&gt;

&lt;p&gt;Reading CSV or JSON directly at runtime is convenient early in development.&lt;/p&gt;

&lt;p&gt;CSV is easy to export from Excel or Google Sheets. JSON represents structure clearly and is easy to inspect while debugging.&lt;/p&gt;

&lt;p&gt;In a production runtime, however, parsing CSV or JSON may introduce additional work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parsing is required.&lt;/li&gt;
&lt;li&gt;String processing increases.&lt;/li&gt;
&lt;li&gt;Values must be converted to their target types.&lt;/li&gt;
&lt;li&gt;Errors appear at runtime.&lt;/li&gt;
&lt;li&gt;GC allocations may increase.&lt;/li&gt;
&lt;li&gt;Loading may take longer.&lt;/li&gt;
&lt;li&gt;Platform-specific behavior may surface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a small dataset, this may not matter. Keeping a lightweight configuration file as JSON can be the simpler choice for debug builds, operations tools, A/B test settings, or other small and frequently inspected data.&lt;/p&gt;

&lt;p&gt;For a mobile game or a long-running live-service project where the amount of static data continually grows, runtime text parsing is often something worth reducing.&lt;/p&gt;

&lt;p&gt;MasterMemory builds the data as a MessagePack binary package ahead of time. At runtime, the client retrieves that binary and constructs a &lt;code&gt;MemoryDatabase&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadMasterBinaryAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MemoryDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Binary data does not make &lt;code&gt;MemoryDatabase&lt;/code&gt; construction free. Large datasets should still be profiled on target hardware. If necessary, the constructor's &lt;code&gt;maxDegreeOfParallelism&lt;/code&gt; option can also be evaluated.&lt;/p&gt;

&lt;p&gt;The runtime path becomes focused on three responsibilities: retrieve the binary, build the database, and query it.&lt;/p&gt;

&lt;p&gt;CSV column resolution, string-to-integer conversion, enum conversion, date parsing, null handling, and empty-string rules no longer need to run as part of every client startup path.&lt;/p&gt;

&lt;p&gt;Unity still needs a project-specific strategy for retrieving the binary.&lt;/p&gt;

&lt;p&gt;MasterMemory's responsibility begins once the binary has been obtained. The project must decide whether it lives in Resources, StreamingAssets, Addressables, an AssetBundle, or a remote content-delivery system.&lt;/p&gt;

&lt;p&gt;The application must also prevent database access before asynchronous initialization completes. If the game supports hot updates or event-specific dataset swaps, the team must define when the active database changes and how long consumers of the previous snapshot may continue to exist.&lt;/p&gt;

&lt;p&gt;Production code should also decide what happens when loading fails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stop startup and show an error&lt;/li&gt;
&lt;li&gt;Retry&lt;/li&gt;
&lt;li&gt;Continue using the last known-good database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single reference point such as &lt;code&gt;MasterDataProvider&lt;/code&gt; can help ensure that every system observes the same database generation. When a binary is obtained as a &lt;code&gt;TextAsset&lt;/code&gt; through Addressables or AssetBundle, the lifetime of the handle and the data required after database construction should also be verified.&lt;/p&gt;

&lt;p&gt;It is safer to avoid caching row objects or &lt;code&gt;RangeView&amp;lt;T&amp;gt;&lt;/code&gt; values from an old database for long periods after a swap.&lt;/p&gt;

&lt;p&gt;A clean division is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Convert and validate data before the build, or in CI.&lt;/li&gt;
&lt;li&gt;Load only validated binary data at runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation makes the runtime code more predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing Validators in C# Is Practical
&lt;/h2&gt;

&lt;p&gt;The most dangerous master-data errors are often not failures to parse. They are values that parse successfully but describe an invalid game state.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A reward item ID that does not exist&lt;/li&gt;
&lt;li&gt;A skill referencing a missing effect ID&lt;/li&gt;
&lt;li&gt;A gap in a level progression table&lt;/li&gt;
&lt;li&gt;A missing localization key&lt;/li&gt;
&lt;li&gt;A start date later than an end date&lt;/li&gt;
&lt;li&gt;Probabilities that do not add up to 100 percent&lt;/li&gt;
&lt;li&gt;Duplicate values inside a group that requires uniqueness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Types alone cannot catch these problems.&lt;/p&gt;

&lt;p&gt;An integer may be valid as an integer while still referencing a nonexistent record. A string may be valid as a string while still naming a missing localization entry.&lt;/p&gt;

&lt;p&gt;MasterMemory includes Validator support.&lt;/p&gt;

&lt;p&gt;The Cygames article about MasterMemory v2 explains that a schema can implement &lt;code&gt;IValidatable&amp;lt;T&amp;gt;&lt;/code&gt;, allowing validation logic to stay close to the data definition. It can validate reference-like relationships, numeric ranges, and rules involving the entire dataset.&lt;/p&gt;

&lt;p&gt;A conceptual example that verifies a reward item reference may look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;MemoryTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"quest"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;MessagePackObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QuestMaster&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IValidatable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;QuestMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PrimaryKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;QuestId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;RewardItemId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;IValidatable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;QuestMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;QuestMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RewardItemId&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetReferenceSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ItemMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// Conceptual example: verify that the current QuestMaster.RewardItemId&lt;/span&gt;
            &lt;span class="c1"&gt;// exists as an ItemMaster.ItemId.&lt;/span&gt;
            &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quest&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;quest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RewardItemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ItemId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Additional validation can still run when RewardItemId &amp;lt;= 0.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validator is easiest to reason about when it runs after database generation, before a build, or as part of the CI conversion pipeline. Its role is to stop invalid data before the game loads it, rather than discovering the problem after the runtime has already started.&lt;/p&gt;

&lt;p&gt;Writing the rules in C# is flexible.&lt;/p&gt;

&lt;p&gt;The team can use IDE completion, refactoring tools, compile-time type checking, and its normal code-review process instead of introducing a separate DSL or rule-file format.&lt;/p&gt;

&lt;p&gt;Game-data validation also tends to exceed simple foreign-key checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Another column is required only when a flag is true.&lt;/li&gt;
&lt;li&gt;Allowed ranges differ by rarity.&lt;/li&gt;
&lt;li&gt;The referenced table depends on the event type.&lt;/li&gt;
&lt;li&gt;Certain ID ranges are reserved.&lt;/li&gt;
&lt;li&gt;A value is allowed in development but forbidden in production.&lt;/li&gt;
&lt;li&gt;Consistency must be checked across several tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those rules are often easier to maintain as normal program logic.&lt;/p&gt;

&lt;p&gt;Validator therefore makes MasterMemory more than a runtime lookup database. It also gives the data pipeline a place to enforce data quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metadata Leaves Room for Surrounding Tools
&lt;/h2&gt;

&lt;p&gt;The official README also describes Metadata APIs that can be used when building custom importers and exporters.&lt;/p&gt;

&lt;p&gt;This is less visible than lookup performance, but it matters in production.&lt;/p&gt;

&lt;p&gt;A complete game-data workflow usually needs more than the runtime database:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A CSV conversion tool&lt;/li&gt;
&lt;li&gt;A Google Sheets importer&lt;/li&gt;
&lt;li&gt;A diff viewer&lt;/li&gt;
&lt;li&gt;HTML or Markdown validation reports&lt;/li&gt;
&lt;li&gt;Readable CI error output&lt;/li&gt;
&lt;li&gt;A debug exporter that converts data back to JSON&lt;/li&gt;
&lt;li&gt;Planner-facing tools that identify the exact source of an error&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When building these tools, it helps to inspect table and column definitions programmatically.&lt;/p&gt;

&lt;p&gt;Metadata makes it possible to build a custom pipeline around MasterMemory.&lt;/p&gt;

&lt;p&gt;That does not mean MasterMemory automatically provides a finished operational platform. It means the library exposes enough structure for teams to build the workflow they need.&lt;/p&gt;

&lt;p&gt;This extensibility becomes valuable when MasterMemory is treated as one component of a larger data pipeline rather than only as a runtime database.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Compares with ScriptableObject
&lt;/h2&gt;

&lt;p&gt;ScriptableObject remains a powerful option for static data in Unity.&lt;/p&gt;

&lt;p&gt;It has a different set of strengths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data is visible in the Unity Editor.&lt;/li&gt;
&lt;li&gt;Designers can edit it through the Inspector.&lt;/li&gt;
&lt;li&gt;It can be referenced as an Asset.&lt;/li&gt;
&lt;li&gt;It integrates naturally with Prefabs and Addressables.&lt;/li&gt;
&lt;li&gt;It can be convenient for artists and level designers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no need to reject ScriptableObject.&lt;/p&gt;

&lt;p&gt;For many kinds of data, ScriptableObject is the better tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A small number of configuration objects&lt;/li&gt;
&lt;li&gt;Data that benefits from references between ScriptableObjects&lt;/li&gt;
&lt;li&gt;Data that should be edited visually in the Unity Editor&lt;/li&gt;
&lt;li&gt;Data that directly references Unity Assets&lt;/li&gt;
&lt;li&gt;Presentation settings for individual levels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Managing a large tabular dataset entirely with ScriptableObject becomes more difficult:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One Asset per record creates many files.&lt;/li&gt;
&lt;li&gt;The data is difficult to view as a complete table.&lt;/li&gt;
&lt;li&gt;Spreadsheet-style editing is awkward for planners.&lt;/li&gt;
&lt;li&gt;Diffs are harder to review.&lt;/li&gt;
&lt;li&gt;Bulk conversion and validation require additional tooling.&lt;/li&gt;
&lt;li&gt;ID and composite-key queries still need to be implemented.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MasterMemory is better suited to large tabular datasets that need fast, type-safe lookup.&lt;/p&gt;

&lt;p&gt;For Asset references, a MasterMemory row can store an Addressables key, GUID, Resources path, or project-specific Asset ID, while another layer resolves that identifier to the actual Unity Asset. Separating tabular game data from Asset resolution often produces a cleaner workflow than forcing both responsibilities into the same system.&lt;/p&gt;

&lt;p&gt;ScriptableObject and MasterMemory are therefore less direct competitors than they first appear. Their strengths differ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use ScriptableObject for a small number of editor-friendly configuration Assets.&lt;/li&gt;
&lt;li&gt;Use MasterMemory for large tabular datasets that require structured, type-safe lookup.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How It Compares with SQLite
&lt;/h2&gt;

&lt;p&gt;SQLite is another established option for a local database.&lt;/p&gt;

&lt;p&gt;It has a long track record, supports flexible SQL queries, and benefits from a mature ecosystem of tools.&lt;/p&gt;

&lt;p&gt;For client-side static game data, however, that flexibility may be more than the project needs.&lt;/p&gt;

&lt;p&gt;Most games do not construct arbitrary SQL queries during gameplay. They usually perform a known set of lookups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find by ID&lt;/li&gt;
&lt;li&gt;Find by composite key&lt;/li&gt;
&lt;li&gt;Retrieve all rows for a group ID&lt;/li&gt;
&lt;li&gt;Retrieve a range&lt;/li&gt;
&lt;li&gt;Retrieve the closest value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For those patterns, type-safe generated APIs, low allocation, fast loading, and compact memory usage may be more useful than unrestricted SQL.&lt;/p&gt;

&lt;p&gt;String-based SQL also means that some query errors remain invisible until runtime.&lt;/p&gt;

&lt;p&gt;MasterMemory defines the lookup patterns ahead of time and exposes generated methods.&lt;/p&gt;

&lt;p&gt;It is less flexible than SQLite, but that trade-off can make it easier to use for read-only game data.&lt;/p&gt;

&lt;p&gt;The point is not that one tool is universally better.&lt;/p&gt;

&lt;p&gt;SQLite may be the right choice when runtime updates or flexible ad hoc queries are required. MasterMemory is a better fit when the application reads a large, predefined dataset through known query paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  When MasterMemory Is—and Is Not—a Good Fit
&lt;/h2&gt;

&lt;p&gt;The following table summarizes the decision:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Consideration&lt;/th&gt;
&lt;th&gt;MasterMemory is a good fit&lt;/th&gt;
&lt;th&gt;Another approach may be simpler&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data size&lt;/td&gt;
&lt;td&gt;Many tables or records&lt;/td&gt;
&lt;td&gt;Only a small amount of data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update frequency&lt;/td&gt;
&lt;td&gt;Mostly immutable during a session&lt;/td&gt;
&lt;td&gt;Mutated frequently at runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries&lt;/td&gt;
&lt;td&gt;Many ID, composite-key, or range queries&lt;/td&gt;
&lt;td&gt;Only a few simple lookups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Startup time, GC, or memory usage matters&lt;/td&gt;
&lt;td&gt;Performance is not currently a concern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type safety&lt;/td&gt;
&lt;td&gt;Query mistakes should be caught at compile time&lt;/td&gt;
&lt;td&gt;Minimal setup is more important&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;Cross-table references and ranges need C# validation&lt;/td&gt;
&lt;td&gt;Manual inspection is sufficient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline&lt;/td&gt;
&lt;td&gt;Conversion and validation should run in CI&lt;/td&gt;
&lt;td&gt;Local manual work is sufficient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing architecture&lt;/td&gt;
&lt;td&gt;Custom Dictionaries have grown complex&lt;/td&gt;
&lt;td&gt;ScriptableObject already works well&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most important considerations are usually data size, lookup patterns, and update frequency.&lt;/p&gt;

&lt;p&gt;If the dataset is large, the game uses more than simple ID lookups, and the active data is effectively immutable during a session, MasterMemory is a natural candidate.&lt;/p&gt;

&lt;p&gt;If ScriptableObject already handles a small dataset comfortably, if the data is temporary and frequently mutated, or if the project requires arbitrary SQL queries, another approach may remain simpler.&lt;/p&gt;

&lt;p&gt;Adoption also has a cost.&lt;/p&gt;

&lt;p&gt;A MasterMemory project needs schema management based on Source Generators and MessagePack attributes. Migrating from ScriptableObject, CSV, or JSON may require conversion tools and a validation pipeline.&lt;/p&gt;

&lt;p&gt;Schema changes also require the team to consider binary regeneration, compatibility, and consistency with already distributed data. In Unity, package updates, generated code, IL2CPP/AOT behavior, and CI generation steps become part of the operational process.&lt;/p&gt;

&lt;p&gt;The Unity version must support the Source Generator workflow, and package installation generally involves NuGetForUnity or an equivalent setup. At the time of writing, the v3.0.4 README lists Unity 2022.3.12f1 as the minimum supported version.&lt;/p&gt;

&lt;p&gt;If examples use C# 9 &lt;code&gt;init&lt;/code&gt; accessors, some Unity configurations may require an &lt;code&gt;IsExternalInit&lt;/code&gt; definition so the compiler can resolve that feature. In IL2CPP builds, the generated &lt;code&gt;MasterMemoryResolver&lt;/code&gt; must also be registered with MessagePack's Resolver configuration.&lt;/p&gt;

&lt;p&gt;This is not an installation tutorial, so the exact setup is left to the official README and version-specific guides. The important point is that a project can appear to work in the Editor and still fail later in an IL2CPP device build if these requirements are ignored.&lt;/p&gt;

&lt;p&gt;MasterMemory is not a magic box that instantly solves every master-data problem.&lt;/p&gt;

&lt;p&gt;What it does provide is a clear owner for the runtime read-only database layer, which makes the rest of the runtime architecture easier to organize.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MasterMemory Does Not Solve
&lt;/h2&gt;

&lt;p&gt;So far, this article has focused primarily on MasterMemory's strengths.&lt;/p&gt;

&lt;p&gt;The remaining areas are not necessarily weaknesses. They are responsibilities outside the scope of the library.&lt;/p&gt;

&lt;p&gt;MasterMemory is effective at providing fast, type-safe runtime access to static data. A production game-data workflow also has stages before and after that runtime database.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Decisions MasterMemory does not make for you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source of truth&lt;/td&gt;
&lt;td&gt;Whether Excel, Google Sheets, or CSV is authoritative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editing workflow&lt;/td&gt;
&lt;td&gt;How planners edit and review changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conversion&lt;/td&gt;
&lt;td&gt;How source rows become C# objects and then a database binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Localization&lt;/td&gt;
&lt;td&gt;How keys, missing translations, per-language CSV files, and translation returns are handled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diff review&lt;/td&gt;
&lt;td&gt;How humans compare revisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debugging&lt;/td&gt;
&lt;td&gt;How a binary record maps back to its source row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD&lt;/td&gt;
&lt;td&gt;How conversion, validation, and failure reporting are automated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environments&lt;/td&gt;
&lt;td&gt;How development, QA, production, and pre-release event data are separated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If Google Sheets is the source of truth, the project still needs code that retrieves the sheets through an API, converts rows to typed objects, and passes them to MasterMemory's &lt;code&gt;DatabaseBuilder&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If Excel is the source of truth, the team still needs rules for reading files, interpreting sheet and column names, handling blank rows and comments, and converting values to their target types.&lt;/p&gt;

&lt;p&gt;Localization is another separate workflow.&lt;/p&gt;

&lt;p&gt;Text keys can be stored in MasterMemory, but the library does not decide how to export per-language CSV files, detect missing translations, remove obsolete keys, exchange files with a translation vendor, or review changes when translations return.&lt;/p&gt;

&lt;p&gt;Binary data is efficient for the runtime but inconvenient for humans to inspect directly.&lt;/p&gt;

&lt;p&gt;A production workflow often needs tools that answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the number of records remain consistent after conversion?&lt;/li&gt;
&lt;li&gt;How was a specific ID transformed?&lt;/li&gt;
&lt;li&gt;What changed since the previous build?&lt;/li&gt;
&lt;li&gt;Did development-only data leak into the production package?&lt;/li&gt;
&lt;li&gt;Is the device actually using the latest dataset?&lt;/li&gt;
&lt;li&gt;Which row in the original spreadsheet caused the validation error?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Validator is useful for data correctness, but planner and QA workflows such as reviewing diffs, inspecting conversion results, and tracing an error back to a source row usually require additional tools.&lt;/p&gt;

&lt;p&gt;CI/CD and environment-specific data require the same kind of explicit design.&lt;/p&gt;

&lt;p&gt;MasterMemory can be the final database that the client reads, but the project still decides which source generates which binary, under what conditions, and where that binary is deployed.&lt;/p&gt;

&lt;p&gt;Leaving those decisions vague can lead to serious mistakes: development data being shipped to production, builds using stale binaries, or unvalidated data entering a release.&lt;/p&gt;

&lt;p&gt;The distinction matters.&lt;/p&gt;

&lt;p&gt;Adopting MasterMemory does not automate the entire lifecycle of game data. It does, however, give the runtime lookup layer a clear and reliable shape.&lt;/p&gt;

&lt;p&gt;Once that layer is stable, the surrounding import, validation, conversion, deployment, diff, and debugging pipeline becomes easier to design deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;MasterMemory is a strong option for Unity and .NET projects that need to handle a large amount of read-only static game data.&lt;/p&gt;

&lt;p&gt;Its Source Generator creates type-safe query APIs, while composite-key, range, and closest-value queries, MessagePack binary loading, Validator, Metadata, and string interning help organize runtime data access. Read-only behavior is also a good fit for game data because the active database can be treated as an immutable snapshot.&lt;/p&gt;

&lt;p&gt;The source of truth in Excel or Google Sheets, planner-facing editing, localization, diff review, CI conversion, environment-specific deployment, and debugging remain responsibilities of the surrounding pipeline.&lt;/p&gt;

&lt;p&gt;That is not a defect so much as a boundary of responsibility.&lt;/p&gt;

&lt;p&gt;Evaluating MasterMemory as a &lt;strong&gt;fast, type-safe, read-only database&lt;/strong&gt;, rather than as a complete master-data management platform, makes adoption decisions much clearer. Its runtime strengths become most valuable when they are combined with a deliberate pipeline for importing, converting, validating, reviewing, and distributing the data around it.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>gamedev</category>
      <category>performance</category>
    </item>
    <item>
      <title>Speed Up Game Tuning with ScriptableObject: Edit and Save Values During Play Mode in the Unity Editor</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Tue, 14 Jul 2026 08:43:44 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/speed-up-game-tuning-with-scriptableobject-edit-and-save-values-during-play-mode-in-the-unity-2oki</link>
      <guid>https://dev.to/gamedevtoollab/speed-up-game-tuning-with-scriptableobject-edit-and-save-values-during-play-mode-in-the-unity-2oki</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;ScriptableObject is often one of the first tools developers reach for when they need to manage data in a Unity project.&lt;/p&gt;

&lt;p&gt;Typical examples include enemy parameters, weapon settings, camera shake, player movement speed, skill cooldowns, and per-stage tuning values.&lt;/p&gt;

&lt;p&gt;Because a ScriptableObject exists as an asset in the Unity Editor, it is easy to inspect, edit, and reference independently from scenes and prefabs. For small to medium-sized configuration data, it is an extremely convenient option.&lt;/p&gt;

&lt;p&gt;Unity's documentation describes ScriptableObject as a data container that exists independently from class instances. It can also reduce duplicated data by allowing multiple prefabs to reference the same asset.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/2022.3/Documentation/Manual/class-ScriptableObject.html" rel="noopener noreferrer"&gt;Unity Manual - ScriptableObject&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, the real value of ScriptableObject is not limited to storing data as an asset.&lt;/p&gt;

&lt;p&gt;One of its most useful properties in day-to-day game development is that you can &lt;strong&gt;change values while the game is running in the Unity Editor and immediately apply those changes to gameplay&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Movement speed, jump force, enemy attack intervals, camera following, knockback, hit-stop, and input-buffer timing rarely have a single correct value from the beginning. You normally decide them by playing the game and checking how they interact with the screen, controls, enemy movement, level size, and animation timing.&lt;/p&gt;

&lt;p&gt;That process becomes slow when every adjustment requires the following cycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Change code.&lt;/li&gt;
&lt;li&gt;Compile.&lt;/li&gt;
&lt;li&gt;Enter Play Mode.&lt;/li&gt;
&lt;li&gt;Test the result.&lt;/li&gt;
&lt;li&gt;Exit Play Mode.&lt;/li&gt;
&lt;li&gt;Change the value again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If tuning values are collected in a ScriptableObject and the game reads them during Play Mode, you can make adjustments from the Inspector without repeatedly restarting the game.&lt;/p&gt;

&lt;p&gt;This article looks at ScriptableObject not only as a place to keep small master-data sets, but as a &lt;strong&gt;practical workflow for speeding up game tuning&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The word "save" in this article refers to development work performed inside the Unity Editor. The samples primarily target Unity 2022.3 LTS and later. For older Unity versions, check whether the Editor APIs used here are available.&lt;/p&gt;

&lt;p&gt;This is not a workflow for modifying a ScriptableObject asset in a player build and using it as user save data. Runtime save data should normally use a separate destination such as JSON, a binary format, PlayerPrefs, cloud storage, or a project-specific database.&lt;/p&gt;

&lt;h2&gt;
  
  
  ScriptableObject Stores Data as a Project Asset
&lt;/h2&gt;

&lt;p&gt;Unlike a MonoBehaviour, a ScriptableObject does not need to be attached to a GameObject. It can exist directly as an asset in the Project window.&lt;/p&gt;

&lt;p&gt;For example, the following asset holds player-tuning values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CreateAssetMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;fileName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PlayerTuning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;menuName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Game/Tuning/Player Tuning"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerTuning&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ScriptableObject&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Move"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;moveSpeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;dashSpeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;8.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;acceleration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;20.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Jump"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;jumpPower&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;12.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;gravityScale&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2.5f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Camera"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;cameraFollowLerp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.12f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create this asset and reference it from a component such as &lt;code&gt;PlayerController&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;PlayerTuning&lt;/span&gt; &lt;span class="n"&gt;tuning&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tuning&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PlayerTuning is not assigned."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;inputX&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAxisRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Horizontal"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;targetSpeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputX&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tuning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;moveSpeed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MoveTowards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;targetSpeed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tuning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;acceleration&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deltaTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;velocity&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deltaTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separates the player's movement parameters from the controller implementation.&lt;/p&gt;

&lt;p&gt;The sample disables the component when &lt;code&gt;tuning&lt;/code&gt; is not assigned, preventing a null reference in &lt;code&gt;Update&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The input code uses the old Input Manager's &lt;code&gt;Input.GetAxisRaw&lt;/code&gt; only to keep the sample short. Replace it with the Input System or whatever input layer your project uses.&lt;/p&gt;

&lt;p&gt;You no longer need to open &lt;code&gt;PlayerController.cs&lt;/code&gt; just to change &lt;code&gt;moveSpeed&lt;/code&gt;. Select the &lt;code&gt;PlayerTuning&lt;/code&gt; asset in the Project window and edit the value in the Inspector.&lt;/p&gt;

&lt;p&gt;When tuning values are scattered across source files, developers spend more time finding them. A ScriptableObject gives the team a clear location: "The tuning values for this feature are in this asset."&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared References Are a Major Advantage
&lt;/h2&gt;

&lt;p&gt;Because a ScriptableObject is an asset, multiple prefabs and components can reference the same instance.&lt;/p&gt;

&lt;p&gt;Suppose a scene creates 100 enemy instances. You can store definitions such as "Slime base parameters" and "Goblin base parameters" in ScriptableObject assets instead of copying the same values into every prefab.&lt;/p&gt;

&lt;p&gt;In this design, the enemy instance owns its current state while the ScriptableObject owns its definition.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current HP, current position, status effects, and the current target are runtime state.&lt;/li&gt;
&lt;li&gt;Maximum HP, attack power, movement speed, and sight range are definitions or tuning values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Separating state from definition allows you to change shared parameters in one place.&lt;/p&gt;

&lt;p&gt;That ability to share one asset across multiple systems is also what makes ScriptableObject useful for tuning during Play Mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Editing During Play Mode Speeds Up Development
&lt;/h2&gt;

&lt;p&gt;The largest practical benefit is how easily ScriptableObject values can be changed and reflected while the game is running.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;PlayerController&lt;/code&gt; reads &lt;code&gt;PlayerTuning&lt;/code&gt; every frame, changing &lt;code&gt;moveSpeed&lt;/code&gt; in the Inspector changes the player's movement speed on the next frame.&lt;/p&gt;

&lt;p&gt;This sounds simple, but it has a large impact on gameplay tuning.&lt;/p&gt;

&lt;p&gt;A common adjustment loop looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Change a value in code or on a prefab.&lt;/li&gt;
&lt;li&gt;Enter Play Mode.&lt;/li&gt;
&lt;li&gt;Test the game.&lt;/li&gt;
&lt;li&gt;Exit Play Mode.&lt;/li&gt;
&lt;li&gt;Change the value again.&lt;/li&gt;
&lt;li&gt;Enter Play Mode again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When values can be changed and applied during Play Mode, the loop becomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enter Play Mode.&lt;/li&gt;
&lt;li&gt;Change values in the Inspector while playing.&lt;/li&gt;
&lt;li&gt;Immediately test the result.&lt;/li&gt;
&lt;li&gt;Save the values that feel right.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can keep the current gameplay context instead of restarting it.&lt;/p&gt;

&lt;p&gt;This is especially valuable when the situation is expensive to reproduce, such as a specific boss phase, a long encounter, or a point deep inside a stage.&lt;/p&gt;

&lt;p&gt;Shorter tuning loops lead to more iterations. In game development, an environment where you can test many variations quickly is often more valuable than trying to guess the perfect value on the first attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the Game to Apply Changes Immediately
&lt;/h2&gt;

&lt;p&gt;Changing a ScriptableObject value during Play Mode does nothing unless the game reads or reapplies that value.&lt;/p&gt;

&lt;p&gt;For example, the following component copies the value once in &lt;code&gt;Awake&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;PlayerTuning&lt;/span&gt; &lt;span class="n"&gt;tuning&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;moveSpeed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;moveSpeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tuning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;moveSpeed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Changing tuning.moveSpeed during Play Mode does not update this value.&lt;/span&gt;
        &lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;moveSpeed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not inherently a bad design. Caching can be useful, and some settings should not be recalculated every frame.&lt;/p&gt;

&lt;p&gt;However, when Play Mode tuning is important, values that should update immediately must either be read directly from the ScriptableObject or reapplied through a dedicated mechanism.&lt;/p&gt;

&lt;p&gt;The simplest option is to read the asset directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tuning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;moveSpeed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another option is to cache the value once for builds but reapply it every frame in the Editor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Apply the initial value in both the Editor and player builds.&lt;/span&gt;
    &lt;span class="nf"&gt;ApplyTuning&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="cp"&gt;#if UNITY_EDITOR
&lt;/span&gt;    &lt;span class="c1"&gt;// Reapply Play Mode changes every frame in the Editor.&lt;/span&gt;
    &lt;span class="nf"&gt;ApplyTuning&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;
    &lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;moveSpeed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ApplyTuning&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;moveSpeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tuning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;moveSpeed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Possible policies include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read lightweight values every frame when immediate updates are useful.&lt;/li&gt;
&lt;li&gt;Use an Apply button when a value requires expensive recalculation.&lt;/li&gt;
&lt;li&gt;Put only the Editor-only reapplication logic behind &lt;code&gt;#if UNITY_EDITOR&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because this example calls &lt;code&gt;ApplyTuning&lt;/code&gt; from &lt;code&gt;Awake&lt;/code&gt;, &lt;code&gt;moveSpeed&lt;/code&gt; is also initialized in a player build. In the Editor it is reapplied every frame, while a build continues using the cached value from startup.&lt;/p&gt;

&lt;p&gt;The final behavior should still be verified under build-equivalent conditions. Editor-only reapplication can hide assumptions that do not exist in the player build.&lt;/p&gt;

&lt;p&gt;ScriptableObject alone does not automatically make a system easy to tune. You must also decide &lt;strong&gt;when a changed value is applied to the running game&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the Save Policy Explicit
&lt;/h2&gt;

&lt;p&gt;Unity's behavior during Play Mode can be confusing.&lt;/p&gt;

&lt;p&gt;Values changed on scene GameObjects and MonoBehaviours usually return to their pre-Play Mode state when Play Mode stops.&lt;/p&gt;

&lt;p&gt;You may find a good value in the Inspector, stop the game, and then discover that the value has reverted because you forgot to write it down.&lt;/p&gt;

&lt;p&gt;ScriptableObject is different because it is an asset.&lt;/p&gt;

&lt;p&gt;When you edit the actual ScriptableObject asset selected in the Project window, you are modifying that asset object even during Play Mode. This is not the same as modifying a runtime copy created with &lt;code&gt;Instantiate&lt;/code&gt;, and you should not assume the value will automatically revert when Play Mode ends.&lt;/p&gt;

&lt;p&gt;The details depend on what is being edited:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A MonoBehaviour on a scene object&lt;/li&gt;
&lt;li&gt;The ScriptableObject asset in the Project window&lt;/li&gt;
&lt;li&gt;A component referencing the original asset&lt;/li&gt;
&lt;li&gt;A runtime copy created with &lt;code&gt;Instantiate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A value modified through &lt;code&gt;SerializedProperty&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A field modified directly from code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These cases can look similar in the Inspector while having different persistence behavior.&lt;/p&gt;

&lt;p&gt;The following discussion assumes normal Play Mode settings. If your project disables Domain Reload or Scene Reload through Enter Play Mode Options, initialization timing and value lifetime can change. Verify the behavior under your project's settings.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/2022.3/Documentation/Manual/ConfigurableEnterPlayMode.html" rel="noopener noreferrer"&gt;Unity Manual - Configurable Enter Play Mode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is also important to distinguish between these two states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ScriptableObject asset object has changed in the Editor.&lt;/li&gt;
&lt;li&gt;The change has been written to the &lt;code&gt;.asset&lt;/code&gt; file on disk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Editing a ScriptableObject in the Project window changes the Editor-side asset object. When the change is committed to disk depends on explicit save operations and the &lt;code&gt;AssetDatabase&lt;/code&gt; workflow.&lt;/p&gt;

&lt;p&gt;In a team environment, use an explicit save action and inspect the version-control diff instead of relying on whether the value appears to remain after stopping Play Mode.&lt;/p&gt;

&lt;p&gt;A workflow such as "adjust runtime values, then press Save only when the result is approved" makes the intent much clearer.&lt;/p&gt;

&lt;p&gt;Relying on an asset accidentally remaining dirty after Play Mode is less predictable than providing an explicit save path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Save Runtime Values Back to a ScriptableObject Asset
&lt;/h2&gt;

&lt;p&gt;The following example adjusts runtime values on a MonoBehaviour and writes the approved values back to a ScriptableObject asset.&lt;/p&gt;

&lt;p&gt;This is Unity Editor-only functionality. Code using the &lt;code&gt;UnityEditor&lt;/code&gt; namespace must be placed in an &lt;code&gt;Editor&lt;/code&gt; folder, guarded by &lt;code&gt;#if UNITY_EDITOR&lt;/code&gt;, or compiled in a separate Editor assembly.&lt;/p&gt;

&lt;p&gt;The workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load initial values from the ScriptableObject asset.&lt;/li&gt;
&lt;li&gt;Adjust runtime values during Play Mode.&lt;/li&gt;
&lt;li&gt;Press a Save button.&lt;/li&gt;
&lt;li&gt;Copy the runtime values to the asset.&lt;/li&gt;
&lt;li&gt;Mark the asset dirty.&lt;/li&gt;
&lt;li&gt;Save the asset to disk.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;EditorUtility.SetDirty&lt;/code&gt; marks an object as dirty. &lt;code&gt;AssetDatabase.SaveAssetIfDirty&lt;/code&gt; saves a specific asset when it is dirty. Because this article targets Unity 2022.3 LTS and later, the main sample uses &lt;code&gt;SaveAssetIfDirty&lt;/code&gt; so that it saves only the target asset.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/2022.3/Documentation/ScriptReference/EditorUtility.SetDirty.html" rel="noopener noreferrer"&gt;EditorUtility.SetDirty&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/2022.3/Documentation/ScriptReference/AssetDatabase.SaveAssetIfDirty.html" rel="noopener noreferrer"&gt;AssetDatabase.SaveAssetIfDirty&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/2022.3/Documentation/ScriptReference/AssetDatabase.SaveAssets.html" rel="noopener noreferrer"&gt;AssetDatabase.SaveAssets&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is one important difference between these APIs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SaveAssetIfDirty&lt;/code&gt; directly saves the specified asset and does not invoke &lt;code&gt;AssetModificationProcessor.OnWillSaveAssets&lt;/code&gt;. If your version-control integration or a custom save hook relies on &lt;code&gt;OnWillSaveAssets&lt;/code&gt;, confirm that &lt;code&gt;SaveAssetIfDirty&lt;/code&gt; is appropriate for the project.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SaveAssets&lt;/code&gt; writes all unsaved asset changes. If a material, prefab, ScriptableObject, or another settings asset is also dirty, it may be saved together with the tuning asset.&lt;/p&gt;

&lt;p&gt;For this reason, it is useful to think of these as two separate operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Copy runtime values into the target asset.&lt;/li&gt;
&lt;li&gt;Flush asset changes to disk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The target in this example is an existing ScriptableObject asset in the Project window. It is not a temporary object created only through &lt;code&gt;ScriptableObject.CreateInstance&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The save destination is the &lt;code&gt;PlayerTuning&lt;/code&gt; asset defined earlier.&lt;/p&gt;

&lt;p&gt;The runtime component keeps its editable values separate from the asset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerTuningRuntime&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;PlayerTuning&lt;/span&gt; &lt;span class="n"&gt;tuningAsset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;moveSpeed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;dashSpeed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;acceleration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;jumpPower&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;gravityScale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;cameraFollowLerp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;PlayerTuning&lt;/span&gt; &lt;span class="n"&gt;TuningAsset&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tuningAsset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tuningAsset&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PlayerTuning asset is not assigned."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nf"&gt;LoadFromAsset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;LoadFromAsset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tuningAsset&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PlayerTuning asset is not assigned."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;moveSpeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tuningAsset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;moveSpeed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;dashSpeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tuningAsset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dashSpeed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;acceleration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tuningAsset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;acceleration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;jumpPower&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tuningAsset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jumpPower&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;gravityScale&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tuningAsset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gravityScale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;cameraFollowLerp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tuningAsset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cameraFollowLerp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fields are public in this sample so they are easy to edit in the default Inspector.&lt;/p&gt;

&lt;p&gt;In a production project, you can replace them with &lt;code&gt;[SerializeField] private&lt;/code&gt; fields and properties, a dedicated tuning DTO, or a custom Inspector.&lt;/p&gt;

&lt;p&gt;The game reads the runtime values rather than the asset values directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;PlayerTuningRuntime&lt;/span&gt; &lt;span class="n"&gt;tuning&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tuning&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;tuning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TuningAsset&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"PlayerTuningRuntime or its PlayerTuning asset is not assigned."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tuning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;moveSpeed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Add the actual movement logic here.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now changing &lt;code&gt;PlayerTuningRuntime.moveSpeed&lt;/code&gt; in the Inspector immediately affects the running game.&lt;/p&gt;

&lt;p&gt;Place the custom Editor in a path such as &lt;code&gt;Assets/Editor/PlayerTuningRuntimeEditor.cs&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEditor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CustomEditor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerTuningRuntime&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerTuningRuntimeEditor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Editor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnInspectorGUI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;DrawDefaultInspector&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerTuningRuntime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TuningAsset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;EditorGUILayout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Space&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;EditorGUI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisabledScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;asset&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;EditorApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isPlaying&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GUILayout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Load From ScriptableObject"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Undo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RecordObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s"&gt;"Load Player Tuning From Asset"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadFromAsset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GUILayout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Save Runtime Values To ScriptableObject"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;SaveToAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SaveToAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;PlayerTuningRuntime&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;PlayerTuning&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Undo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RecordObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Save Player Tuning"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;moveSpeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;moveSpeed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dashSpeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dashSpeed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;acceleration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;acceleration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jumpPower&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jumpPower&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gravityScale&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gravityScale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cameraFollowLerp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clamp01&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cameraFollowLerp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;EditorUtility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetDirty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;AssetDatabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAssetIfDirty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The buttons are enabled only during Play Mode.&lt;/p&gt;

&lt;p&gt;Pressing Save copies the runtime values to the &lt;code&gt;PlayerTuning&lt;/code&gt; asset, marks the asset dirty, and saves that specific asset to disk through &lt;code&gt;SaveAssetIfDirty&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After Play Mode stops, the saved values remain in the &lt;code&gt;.asset&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;The sample also clamps values before saving them.&lt;/p&gt;

&lt;p&gt;Attributes such as &lt;code&gt;[Min]&lt;/code&gt; and &lt;code&gt;[Range]&lt;/code&gt; are useful Inspector aids, but they do not guarantee that code or debug tooling cannot assign an invalid value.&lt;/p&gt;

&lt;p&gt;A production validation layer should also consider relationships between values, units, and upper bounds. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dashSpeed&lt;/code&gt; should be greater than or equal to &lt;code&gt;moveSpeed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jumpPower&lt;/code&gt; and &lt;code&gt;gravityScale&lt;/code&gt; should produce a valid jump curve.&lt;/li&gt;
&lt;li&gt;Camera interpolation values should stay within a project-defined range.&lt;/li&gt;
&lt;li&gt;Values that affect physics should use consistent units.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If project-specific version-control integration or save hooks require &lt;code&gt;AssetDatabase.SaveAssets&lt;/code&gt;, document that it can save every dirty asset, not only the tuning asset.&lt;/p&gt;

&lt;p&gt;The Save timing is explicit, so you can experiment freely and commit only values that you have approved.&lt;/p&gt;

&lt;p&gt;This sample intentionally targets Play Mode tuning. If the same custom Editor must also support Edit Mode or prefab-instance editing, account for &lt;code&gt;SerializedObject&lt;/code&gt;, &lt;code&gt;SerializedProperty&lt;/code&gt;, prefab overrides, scene dirty state, and the relevant Undo behavior.&lt;/p&gt;

&lt;p&gt;There is another maintenance trade-off in this example.&lt;/p&gt;

&lt;p&gt;The same field list appears in &lt;code&gt;PlayerTuning&lt;/code&gt;, &lt;code&gt;PlayerTuningRuntime&lt;/code&gt;, &lt;code&gt;LoadFromAsset&lt;/code&gt;, and &lt;code&gt;SaveToAsset&lt;/code&gt;. Adding a new parameter requires updating every related location.&lt;/p&gt;

&lt;p&gt;Treat this synchronization as reviewable code, or consider a shared synchronization method, generic copying through &lt;code&gt;SerializedObject&lt;/code&gt;, or source generation when the parameter set becomes large.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Undo.RecordObject&lt;/code&gt; during Play Mode should also be treated as a convenience for the current Play session. Do not assume that the Undo stack will remain useful after leaving Play Mode. Verify saved values through the version-control diff and revert them there when necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Directly Editing the ScriptableObject Asset
&lt;/h2&gt;

&lt;p&gt;The previous workflow keeps experimental values on a MonoBehaviour and copies them to the ScriptableObject only when Save is pressed.&lt;/p&gt;

&lt;p&gt;A simpler alternative is to select the ScriptableObject asset and edit it directly during Play Mode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;PlayerTuning&lt;/span&gt; &lt;span class="n"&gt;tuning&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tuning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;moveSpeed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Select the &lt;code&gt;PlayerTuning&lt;/code&gt; asset in the Project window and change &lt;code&gt;moveSpeed&lt;/code&gt; while the game is running.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;PlayerController&lt;/code&gt; reads &lt;code&gt;tuning.moveSpeed&lt;/code&gt; every frame, the new value is applied immediately.&lt;/p&gt;

&lt;p&gt;This method is convenient, but you are editing the original project asset during Play Mode. Treat the Editor-side asset change and the on-disk save as separate concerns, and use explicit save or discard actions together with version-control diffs.&lt;/p&gt;

&lt;p&gt;A runtime copy created with &lt;code&gt;Instantiate&lt;/code&gt; does not write back to the original asset. Editing the original asset does.&lt;/p&gt;

&lt;p&gt;That means an extreme debug value can remain in the project or accidentally appear in a commit.&lt;/p&gt;

&lt;p&gt;A practical comparison looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Good fit&lt;/th&gt;
&lt;th&gt;Main concern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Edit the ScriptableObject asset directly&lt;/td&gt;
&lt;td&gt;Small teams, local experimentation, simple settings&lt;/td&gt;
&lt;td&gt;You are modifying the original asset, so save explicitly and inspect the version-control diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tune runtime values and save them with a button&lt;/td&gt;
&lt;td&gt;Team development and safer experimentation&lt;/td&gt;
&lt;td&gt;Requires a small Editor extension&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create a runtime copy of the ScriptableObject&lt;/td&gt;
&lt;td&gt;Keep the original asset untouched during testing&lt;/td&gt;
&lt;td&gt;Requires an explicit copy-back step when saving&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For gameplay feel, I generally prefer "edit runtime values, then press Save when the result is good."&lt;/p&gt;

&lt;p&gt;As the team grows, explicit persistence usually causes fewer accidents than implicit asset changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How This Differs from Saving MonoBehaviour &lt;code&gt;[SerializeField]&lt;/code&gt; Values
&lt;/h2&gt;

&lt;p&gt;A MonoBehaviour can also contain &lt;code&gt;[SerializeField]&lt;/code&gt; values that are edited during Play Mode and written somewhere through a Save button.&lt;/p&gt;

&lt;p&gt;However, values on a scene MonoBehaviour normally revert when Play Mode stops.&lt;/p&gt;

&lt;p&gt;To preserve them, you must copy the values before stopping into a persistent destination such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A prefab&lt;/li&gt;
&lt;li&gt;A ScriptableObject&lt;/li&gt;
&lt;li&gt;An external file&lt;/li&gt;
&lt;li&gt;Another Editor-managed asset&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern is useful for scene-specific data such as stage gimmicks, Timeline-driven sequences, or camera choreography.&lt;/p&gt;

&lt;p&gt;When the same values must be shared by multiple scenes or prefabs, keeping them on a MonoBehaviour can become difficult to manage.&lt;/p&gt;

&lt;p&gt;Neither approach is universally better.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a MonoBehaviour for scene-specific values.&lt;/li&gt;
&lt;li&gt;Use a ScriptableObject for definitions shared by multiple scenes, prefabs, or systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Player movement feel, weapon recoil, enemy-type parameters, and global difficulty multipliers are often natural ScriptableObject candidates.&lt;/p&gt;

&lt;p&gt;A camera sequence unique to one stage may be more naturally stored on a MonoBehaviour or Timeline.&lt;/p&gt;

&lt;p&gt;The useful distinction is responsibility, not which type is "superior."&lt;/p&gt;

&lt;h2&gt;
  
  
  Data That Fits ScriptableObject Well
&lt;/h2&gt;

&lt;p&gt;ScriptableObject is especially useful for data that developers want to inspect and tune inside the Unity Editor.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Player controls&lt;/li&gt;
&lt;li&gt;Camera following&lt;/li&gt;
&lt;li&gt;Camera shake&lt;/li&gt;
&lt;li&gt;Base parameters per enemy type&lt;/li&gt;
&lt;li&gt;Small to medium weapon or skill data&lt;/li&gt;
&lt;li&gt;UI animation settings&lt;/li&gt;
&lt;li&gt;Difficulty multipliers&lt;/li&gt;
&lt;li&gt;Debug balance settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is particularly strong when the data naturally references Unity assets.&lt;/p&gt;

&lt;p&gt;Sprite, prefab, AudioClip, AnimationClip, AnimationCurve, and Gradient references can be assigned directly in the Inspector.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CreateAssetMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;fileName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"WeaponData"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;menuName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Game/Master/Weapon Data"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WeaponData&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ScriptableObject&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;weaponId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;coolTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Sprite&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;hitEffectPrefab&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;AudioClip&lt;/span&gt; &lt;span class="n"&gt;attackSe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Excel or CSV, those references usually need to be represented through a path, GUID, Addressables key, or another project-specific identifier.&lt;/p&gt;

&lt;p&gt;For settings that include gameplay feel and Unity-asset references, the clarity of ScriptableObject can directly improve development speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do Not Force Every Data Set into ScriptableObject
&lt;/h2&gt;

&lt;p&gt;ScriptableObject is useful, but it becomes awkward when every large table is represented as ScriptableObject assets.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Items&lt;/li&gt;
&lt;li&gt;Skills&lt;/li&gt;
&lt;li&gt;Quests&lt;/li&gt;
&lt;li&gt;Rewards&lt;/li&gt;
&lt;li&gt;Shops&lt;/li&gt;
&lt;li&gt;Gacha data&lt;/li&gt;
&lt;li&gt;Progression tables&lt;/li&gt;
&lt;li&gt;Localization text&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At some point, planners may want to edit thousands of rows in Excel or Google Sheets. The project may also need ID lookup, composite keys, range queries, reference validation, and CI checks.&lt;/p&gt;

&lt;p&gt;At that scale, common problems include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too many assets&lt;/li&gt;
&lt;li&gt;Poor overview of the full table&lt;/li&gt;
&lt;li&gt;Noisy version-control diffs&lt;/li&gt;
&lt;li&gt;Conflicts during simultaneous editing&lt;/li&gt;
&lt;li&gt;Difficult bulk validation&lt;/li&gt;
&lt;li&gt;Custom lookup dictionaries scattered through the project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not reduce the design to "ScriptableObject or Excel" or "ScriptableObject or MasterMemory."&lt;/p&gt;

&lt;p&gt;A hybrid approach is often better.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Player movement, camera shake, hit-stop, and presentation values stay in ScriptableObject assets.&lt;/li&gt;
&lt;li&gt;Thousands of item rows, localization records, progression tables, and reward tables are managed in Excel or Google Sheets and converted for runtime use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ScriptableObject should not be treated as a temporary solution that only belongs at the beginning of a project.&lt;/p&gt;

&lt;p&gt;As a &lt;strong&gt;gameplay-tuning layer&lt;/strong&gt;, it can remain useful late into production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical ScriptableObject Pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Do Not Put Too Much Runtime State in Shared Assets
&lt;/h3&gt;

&lt;p&gt;A ScriptableObject is shared by reference.&lt;/p&gt;

&lt;p&gt;That is useful for definitions, but dangerous for per-instance state.&lt;/p&gt;

&lt;p&gt;For example, storing the enemy's current HP in a shared &lt;code&gt;EnemyParameter&lt;/code&gt; asset causes every enemy referencing that asset to share the same current HP.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EnemyParameter&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ScriptableObject&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxHp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Dangerous: this is runtime state, not a shared definition.&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;currentHp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Current HP, current position, status effects, and the active target belong on the enemy instance or in a dedicated runtime-state object.&lt;/p&gt;

&lt;p&gt;ScriptableObject is safest when it contains definitions, tuning values, and shared settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Be Conscious of Dirtying Project Assets
&lt;/h3&gt;

&lt;p&gt;Editing a ScriptableObject asset in the Editor marks the asset dirty.&lt;/p&gt;

&lt;p&gt;Decide whether the team edits the original asset directly or works on runtime values and writes them back only through an explicit Save button.&lt;/p&gt;

&lt;p&gt;The latter requires more tooling, but it makes the decision to persist a change clear.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Keep Editor Code Out of Player Builds
&lt;/h3&gt;

&lt;p&gt;Code that uses &lt;code&gt;UnityEditor&lt;/code&gt; cannot be included in a player build.&lt;/p&gt;

&lt;p&gt;Place Editor extensions in an &lt;code&gt;Editor&lt;/code&gt; folder or guard them with &lt;code&gt;#if UNITY_EDITOR&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Assets/
  Scripts/
    Runtime/
      PlayerTuning.cs
      PlayerTuningRuntime.cs
      PlayerController.cs
    Editor/
      PlayerTuningRuntimeEditor.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When using assembly definitions, separating the Runtime and Editor assemblies is even safer.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Make the Save Target Obvious
&lt;/h3&gt;

&lt;p&gt;A Save button should clearly state what it saves and where the data goes.&lt;/p&gt;

&lt;p&gt;A button named only &lt;code&gt;Save&lt;/code&gt; could refer to a scene, prefab, ScriptableObject, or external file.&lt;/p&gt;

&lt;p&gt;A longer label such as &lt;code&gt;Save Runtime Values To ScriptableObject&lt;/code&gt; is safer because its purpose is explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  When MasterMemory Becomes a Better Fit
&lt;/h2&gt;

&lt;p&gt;A different system becomes worth considering when the project needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large tables edited in Excel or Google Sheets&lt;/li&gt;
&lt;li&gt;Frequent ID or composite-key lookup&lt;/li&gt;
&lt;li&gt;CI validation of references and data constraints&lt;/li&gt;
&lt;li&gt;Lower parsing cost and fewer runtime allocations&lt;/li&gt;
&lt;li&gt;A type-safe, read-only runtime database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a competition between tools.&lt;/p&gt;

&lt;p&gt;ScriptableObject is excellent for accelerating gameplay tuning. MasterMemory is designed for large, read-only master-data sets that need fast, type-safe queries.&lt;/p&gt;

&lt;p&gt;In the next article, I will examine what MasterMemory solves, what it does not solve, and where it fits in a practical Unity data pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;ScriptableObject is more than a container for static data.&lt;/p&gt;

&lt;p&gt;Its practical strengths are that it is visible in the Unity Editor, can be shared as an asset, and can feed changes into a running game during Play Mode.&lt;/p&gt;

&lt;p&gt;Movement speed, jump force, camera interpolation, enemy attack intervals, hit-stop, camera shake, and UI timing are often faster to tune while playing than through repeated code edits and restarts.&lt;/p&gt;

&lt;p&gt;A small Editor extension can add an explicit Save button so that approved runtime values are written back to a ScriptableObject asset.&lt;/p&gt;

&lt;p&gt;When implementing that workflow, use APIs such as &lt;code&gt;Undo.RecordObject&lt;/code&gt;, &lt;code&gt;EditorUtility.SetDirty&lt;/code&gt;, and &lt;code&gt;AssetDatabase.SaveAssetIfDirty&lt;/code&gt;, and keep the Editor-only code out of player builds.&lt;/p&gt;

&lt;p&gt;At the same time, do not force large tabular data, localization, composite-key queries, CI validation, and spreadsheet integration into ScriptableObject.&lt;/p&gt;

&lt;p&gt;Use each tool for the job it handles best.&lt;/p&gt;

&lt;p&gt;ScriptableObject is a powerful way to shorten the gameplay-tuning loop. Used in its natural role, it can make Unity data management and iteration considerably easier.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>gamedev</category>
      <category>scriptableobject</category>
    </item>
    <item>
      <title>Unity Save Data Writing Tips: Make Local Saves Harder to Break and Easier to Recover</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Tue, 14 Jul 2026 08:38:24 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/unity-save-data-writing-tips-make-local-saves-harder-to-break-and-easier-to-recover-5bi9</link>
      <guid>https://dev.to/gamedevtoollab/unity-save-data-writing-tips-make-local-saves-harder-to-break-and-easier-to-recover-5bi9</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you only need to save a few small settings in Unity, &lt;code&gt;PlayerPrefs&lt;/code&gt; is usually the natural first option.&lt;/p&gt;

&lt;p&gt;For example, saving volume and vibration settings can be as simple as this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"option.masterVolume"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentMasterVolume&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"option.vibration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentVibration&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;loadedMasterVolume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"option.masterVolume"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;loadedVibration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"option.vibration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For prototypes, or for a small number of independent option values, this is often enough.&lt;/p&gt;

&lt;p&gt;However, &lt;code&gt;PlayerPrefs.Save()&lt;/code&gt; is not something you should call for every frequent input event, such as every &lt;code&gt;OnValueChanged&lt;/code&gt; call from a slider.&lt;br&gt;
It is better to call it when the value is confirmed: when the user presses OK, closes the options screen, or when the app is about to pause as a final safeguard.&lt;/p&gt;

&lt;p&gt;First, decide whether &lt;code&gt;PlayerPrefs&lt;/code&gt; is enough for the data you are saving.&lt;/p&gt;

&lt;p&gt;In a released game, save data tends to grow over time.&lt;br&gt;
What started as only a volume setting may later include language, display settings, tutorial progress, read flags, and other state.&lt;br&gt;
Once that happens, &lt;code&gt;PlayerPrefs&lt;/code&gt; becomes awkward for consistency, migration, and recovery.&lt;/p&gt;

&lt;p&gt;The next common option is to write a JSON file under &lt;code&gt;Application.persistentDataPath&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonUtility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saveData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;persistentDataPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"save.json"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But "convert it to JSON and write it" is not the end of the story.&lt;br&gt;
There are several quiet traps: quit-time saves may not run, the app may be killed during a write, failed loads may accidentally overwrite recoverable data with defaults, and multiple save paths may overwrite newer data with older data.&lt;/p&gt;

&lt;p&gt;Save data is not flashy, but when it breaks, the user experience is awful.&lt;br&gt;
Even small data, such as volume or language settings, becomes frustrating if it resets every time.&lt;/p&gt;

&lt;p&gt;This article collects practical notes for writing local saves and option settings in Unity.&lt;/p&gt;

&lt;p&gt;This is not an article about creating a save system that can never break.&lt;br&gt;
The goal is to reduce the chance of save failure, and to make recovery easier when failure does happen.&lt;/p&gt;

&lt;p&gt;The main target is mobile Unity projects, including Android.&lt;br&gt;
That said, most of the ideas apply to PC builds as well.&lt;/p&gt;
&lt;h2&gt;
  
  
  Scope of this article
&lt;/h2&gt;

&lt;p&gt;This article focuses on lightweight local data such as option settings, tutorial progress, simple local save data, and cache metadata.&lt;/p&gt;

&lt;p&gt;It does not cover full anti-cheat design, server synchronization, guarantees for paid items or currency, cloud save conflict resolution, or large custom binary formats.&lt;/p&gt;

&lt;p&gt;In this article, "safe" mainly means the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make files less likely to break during writes&lt;/li&gt;
&lt;li&gt;Avoid relying too much on app quit events&lt;/li&gt;
&lt;li&gt;Make failed loads easier to recover from&lt;/li&gt;
&lt;li&gt;Avoid accidental overwrites caused by implementation mistakes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once data is stored locally, you should assume the user can access or modify it.&lt;br&gt;
Important currency, paid items, rankings, and values that affect competitive play should not trust local saves as the source of truth.&lt;/p&gt;
&lt;h2&gt;
  
  
  Keep PlayerPrefs for small use cases
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;PlayerPrefs&lt;/code&gt; is not a bad API.&lt;br&gt;
For small settings, it is convenient and often the right tool.&lt;/p&gt;

&lt;p&gt;It can be fine for values such as master volume, vibration, graphics preset, last selected language, or a simple "tutorial already shown" flag.&lt;/p&gt;

&lt;p&gt;The decision should be based on operational requirements, not only on the item name.&lt;br&gt;
A single language setting can be fine in &lt;code&gt;PlayerPrefs&lt;/code&gt;.&lt;br&gt;
But if you need to manage language together with other settings, progress, read flags, migration, and recovery, it is safer to move that state into a JSON file.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PlayerPrefs&lt;/code&gt; stores values by key.&lt;br&gt;
It is not a great fit when you want to treat multiple values as one coherent save file.&lt;br&gt;
It also makes it harder to express requirements such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrate based on a save &lt;code&gt;version&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Recover from a &lt;code&gt;.bak&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Avoid overwriting immediately when a load fails&lt;/li&gt;
&lt;li&gt;Keep broken files for investigation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also, this is not specific to &lt;code&gt;PlayerPrefs&lt;/code&gt;: local data should be treated as user-editable.&lt;br&gt;
Do not use &lt;code&gt;PlayerPrefs&lt;/code&gt; alone as the source of truth for paid items, currency, rankings, or values that affect multiplayer or competitive results.&lt;/p&gt;

&lt;p&gt;A practical split is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;PlayerPrefs&lt;/code&gt; for a small number of independent settings&lt;/li&gt;
&lt;li&gt;Use JSON files for grouped state, migrated state, or data that should be recoverable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From here on, we will assume that the data has grown beyond what should live in &lt;code&gt;PlayerPrefs&lt;/code&gt;, and that we are saving JSON files under &lt;code&gt;persistentDataPath&lt;/code&gt;.&lt;br&gt;
First, let us look at the simplest direct file write approach many projects start with.&lt;/p&gt;
&lt;h2&gt;
  
  
  Use persistentDataPath as the default location
&lt;/h2&gt;

&lt;p&gt;When you need to save data created at runtime in Unity, &lt;code&gt;Application.persistentDataPath&lt;/code&gt; is usually the first place to consider.&lt;br&gt;
Unity describes it as a directory for data that should persist across app launches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.IO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SaveFiles&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;OptionsPath&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;persistentDataPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"options.json"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ProgressPath&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;persistentDataPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"progress.json"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, &lt;code&gt;persistentDataPath&lt;/code&gt; does not mean "this data can never disappear".&lt;/p&gt;

&lt;p&gt;With a normal app update, data is expected to remain as long as the bundle identifier stays the same.&lt;br&gt;
But data may be lost if the bundle identifier changes, the app is uninstalled, the user clears app data, a backup is restored, or the storage state changes.&lt;/p&gt;

&lt;p&gt;So loading code should treat "the file does not exist" as a normal case.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;CreateDefaultOptions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In some cases, local code cannot tell whether this is the first launch or whether the user deleted the data.&lt;br&gt;
For important data, you need to think about consistency with server-side data or cloud saves as well.&lt;/p&gt;
&lt;h2&gt;
  
  
  JSON is convenient, but use a save DTO
&lt;/h2&gt;

&lt;p&gt;For lightweight data, JSON is easy to inspect and debug.&lt;br&gt;
For small data such as option settings, Unity's &lt;code&gt;JsonUtility&lt;/code&gt; is often enough.&lt;/p&gt;

&lt;p&gt;For example, option settings can be represented like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Serializable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OptionSaveData&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;masterVolume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;bgmVolume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;seVolume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ja"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;vibration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonUtility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optionData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prettyPrint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SaveFiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OptionsPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;JsonUtility&lt;/code&gt; uses Unity's serializer, keep these practical constraints in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;[Serializable]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Prefer fields for saved values&lt;/li&gt;
&lt;li&gt;Avoid property-centered save classes&lt;/li&gt;
&lt;li&gt;Dictionaries, complex types, and top-level arrays are awkward&lt;/li&gt;
&lt;li&gt;Prepare migration when deleting or renaming fields&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;version&lt;/code&gt; field from the beginning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A property-centered class like this is not a good save DTO for &lt;code&gt;JsonUtility&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Serializable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BadOptionData&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;MasterVolume&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;BgmVolume&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The class you use inside the game and the DTO you serialize do not have to be the same class.&lt;br&gt;
Trying to force them to be one class often makes future changes harder.&lt;/p&gt;
&lt;h2&gt;
  
  
  Do not write directly; write to a temp file and replace
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;File.WriteAllText&lt;/code&gt; is convenient, but direct overwriting is scary for existing save files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the app crashes during the write, the device loses power, or the OS kills the process, a partially written file may remain.&lt;/p&gt;

&lt;p&gt;For example, you may end up with broken JSON like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"masterVolume"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bgmVolume"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the next launch, loading fails.&lt;br&gt;
If the failure path is careless, the game may write default data immediately and destroy any chance of recovery.&lt;/p&gt;

&lt;p&gt;A common improvement is to write to a temporary file first, then replace the main file.&lt;/p&gt;

&lt;p&gt;The basic flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write the new contents to &lt;code&gt;save.json.tmp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Flush the written contents&lt;/li&gt;
&lt;li&gt;Move the existing &lt;code&gt;save.json&lt;/code&gt; to &lt;code&gt;save.json.bak&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Move &lt;code&gt;save.json.tmp&lt;/code&gt; to &lt;code&gt;save.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Delete &lt;code&gt;.bak&lt;/code&gt; after success&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Android has an &lt;code&gt;AtomicFile&lt;/code&gt; API based on a similar idea: write first, then commit by renaming.&lt;br&gt;
The Unity C# sample below does not provide the same guarantees, but the idea is useful.&lt;/p&gt;

&lt;p&gt;The following is a minimal explanatory sample.&lt;br&gt;
This is not an "atomic save" implementation.&lt;br&gt;
It is a best-effort approach that improves recoverability.&lt;br&gt;
In production, centralize the save entry point and serialize writes to the same &lt;code&gt;path&lt;/code&gt;.&lt;br&gt;
Fixed &lt;code&gt;.tmp&lt;/code&gt; and &lt;code&gt;.bak&lt;/code&gt; names are also vulnerable to leftover files from previous failures and concurrent saves.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.IO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SafeFileWriter&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;WriteAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Explanatory sample.&lt;/span&gt;
        &lt;span class="c1"&gt;// Do not call this concurrently for the same path.&lt;/span&gt;
        &lt;span class="c1"&gt;// The caller should serialize writes and combine this with startup recovery.&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDirectoryName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invalid path."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tempPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;".tmp"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;backupPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;".bak"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nf"&gt;WriteTemp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tempPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backupPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backupPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backupPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tempPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backupPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backupPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;TryRestore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backupPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;WriteTemp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;tempPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FileStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;tempPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;FileMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;FileAccess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;FileShare&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Flush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flushToDisk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;TryRestore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;backupPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backupPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backupPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sample does not guarantee a fully atomic replace on every platform.&lt;br&gt;
Because it uses fixed &lt;code&gt;.tmp&lt;/code&gt; and &lt;code&gt;.bak&lt;/code&gt; names, avoid parallel saves to the same file and use it together with a centralized save entry point or per-path serialization.&lt;br&gt;
There is a moment during replacement where the main file may temporarily not exist, so startup recovery is also required.&lt;br&gt;
&lt;code&gt;Flush(flushToDisk: true)&lt;/code&gt; helps flush file contents, but it does not magically make directory entries after rename fully crash-proof.&lt;/p&gt;

&lt;p&gt;If your target environment supports it, &lt;code&gt;File.Replace(tempPath, path, backupPath)&lt;/code&gt; can be another option.&lt;br&gt;
However, you should verify runtime support, OS behavior, and exception behavior on your actual Unity target platforms.&lt;/p&gt;

&lt;p&gt;The important point is to create the temp file in the same directory as the main file.&lt;br&gt;
If you cross drives, mounts, or directories, a rename may behave more like a copy, which can reduce safety.&lt;/p&gt;
&lt;h2&gt;
  
  
  Recover from tmp and bak on startup
&lt;/h2&gt;

&lt;p&gt;If you use temp-file replacement, also prepare startup recovery.&lt;/p&gt;

&lt;p&gt;If the previous save failed halfway, you may find files like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;save.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;save.json.tmp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;save.json.bak&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simple policy is enough for many projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the main file can be read, use it&lt;/li&gt;
&lt;li&gt;If the main file cannot be read but the backup can be read, use the backup&lt;/li&gt;
&lt;li&gt;Basically delete the temp file&lt;/li&gt;
&lt;li&gt;If recovery succeeds, rewrite the main file if possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this sample, &lt;code&gt;.tmp&lt;/code&gt; is not treated as a committed save result.&lt;br&gt;
The design prioritizes consistency over latestness.&lt;br&gt;
If you want to recover from &lt;code&gt;.tmp&lt;/code&gt; as a possible latest save, design extra metadata such as generation numbers, checksums, or commit markers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;OptionSaveData&lt;/span&gt; &lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SaveFiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OptionsPath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tempPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;".tmp"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;backupPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;".bak"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;TryDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tempPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;TryLoad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;TryLoad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backupPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;TrySaveRecoveredData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Best-effort rewrite&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OptionSaveData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;TrySaveRecoveredData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OptionSaveData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Recovered save data, but failed to rewrite main file.\n&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Treat the load itself as successful.&lt;/span&gt;
        &lt;span class="c1"&gt;// If needed, mark the data dirty and retry later.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key point is: do not overwrite with default data the moment loading fails.&lt;br&gt;
Returning defaults may be fine, but saving them immediately can overwrite a broken main file or backup and remove any chance of investigation or recovery.&lt;/p&gt;

&lt;p&gt;If the backup can be loaded, start the game with that data first.&lt;br&gt;
Rewriting the main file should be best effort.&lt;/p&gt;

&lt;p&gt;So far, we have looked at where to save, what format to use, and how to write.&lt;br&gt;
Next is when to save.&lt;/p&gt;
&lt;h2&gt;
  
  
  Do not trust app-quit saves too much
&lt;/h2&gt;

&lt;p&gt;A common save design is to write everything when the app quits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnApplicationQuit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This may be acceptable for small PC tools, but it should not be your main strategy on mobile.&lt;/p&gt;

&lt;p&gt;Unity's &lt;code&gt;Application.quitting&lt;/code&gt; documentation notes that Android does not detect quitting when the app is paused.&lt;br&gt;
It recommends using &lt;code&gt;OnApplicationFocus(bool)&lt;/code&gt; or &lt;code&gt;OnApplicationPause(bool)&lt;/code&gt; instead for those cases.&lt;br&gt;
The &lt;code&gt;OnApplicationQuit&lt;/code&gt; documentation also notes that on iOS, applications are usually suspended instead of quit, and suggests using &lt;code&gt;OnApplicationPause&lt;/code&gt; for suspension and cleanup.&lt;/p&gt;

&lt;p&gt;In other words, "save when the app quits" is a weak assumption on mobile.&lt;/p&gt;

&lt;p&gt;Users do not always explicitly quit the app.&lt;br&gt;
They press Home, switch apps, receive phone calls, get killed by the OS, or hit a crash.&lt;/p&gt;

&lt;p&gt;This kind of design is risky:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetMasterVolume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;masterVolume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnApplicationQuit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user changes the volume, but the value is only written when the app quits.&lt;br&gt;
If the app is killed before that, or the quit event never arrives, the change is lost.&lt;/p&gt;

&lt;p&gt;Treat quit-time saving as the last safety net, not the main save timing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Save option settings at confirmation points
&lt;/h2&gt;

&lt;p&gt;Option settings such as volume, language, vibration, and graphics quality have clear save timing.&lt;br&gt;
Instead of saving every time a slider moves, update the UI immediately and save when the user presses OK, applies the change, or closes the screen.&lt;br&gt;
This also makes the design easier to extend later if saves become asynchronous or tied to cloud sync.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OptionController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;OptionSaveData&lt;/span&gt; &lt;span class="n"&gt;_current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;OptionSaveData&lt;/span&gt; &lt;span class="n"&gt;_editing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;OptionSaveService&lt;/span&gt; &lt;span class="n"&gt;_saveService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetMasterVolume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_editing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;masterVolume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;AudioListener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;volume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Apply immediately&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Apply&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_editing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_saveService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_editing&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;AudioListener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;volume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;masterVolume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When canceling, restore not only the visible UI or audio output, but also the editing data.&lt;br&gt;
If &lt;code&gt;_editing&lt;/code&gt; keeps the changed value, it may be saved later when the screen is opened again or when another option is applied.&lt;/p&gt;

&lt;p&gt;With this structure, OK saves, Cancel discards, and leaving the screen can also be made into a clear save point.&lt;/p&gt;

&lt;p&gt;If you choose to save immediately when a value changes, it is still often better to debounce the request.&lt;br&gt;
For example, save after 0.5 seconds without further slider movement.&lt;/p&gt;

&lt;p&gt;You do not have to make the first version complex.&lt;br&gt;
Saving when the options screen closes, or when the user presses OK, is already much safer than relying on app quit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Save progress at meaningful milestones
&lt;/h2&gt;

&lt;p&gt;Game progress should also avoid relying only on app quit.&lt;/p&gt;

&lt;p&gt;Choose save points based on where the player would be unhappy to be rolled back.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;th&gt;Save timing example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tutorial progress&lt;/td&gt;
&lt;td&gt;When each step is completed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stage progress&lt;/td&gt;
&lt;td&gt;Clear, checkpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inventory item&lt;/td&gt;
&lt;td&gt;When acquisition or consumption is confirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shop purchase&lt;/td&gt;
&lt;td&gt;When purchase is confirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quest reward&lt;/td&gt;
&lt;td&gt;When reward is received&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Option setting&lt;/td&gt;
&lt;td&gt;OK, Apply, screen close&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the stage clear animation plays, the player quits, and the next launch treats the stage as uncleared, that feels terrible.&lt;br&gt;
On the other hand, saving every enemy position and projectile state every frame is usually unnecessary.&lt;/p&gt;

&lt;p&gt;Define how far rollback is acceptable, and save at that boundary.&lt;/p&gt;
&lt;h2&gt;
  
  
  Use OnApplicationPause as a safeguard
&lt;/h2&gt;

&lt;p&gt;On mobile, it is common to save when the app moves to the background using &lt;code&gt;OnApplicationPause(true)&lt;/code&gt; or &lt;code&gt;OnApplicationFocus(false)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is useful.&lt;br&gt;
But it should not be the only save timing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnApplicationPause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;SaveIfDirty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnApplicationFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;SaveIfDirty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normally, save at user actions and game milestones first.&lt;br&gt;
Then, when the app is about to move to the background, save any remaining dirty data.&lt;/p&gt;

&lt;p&gt;Keep background-transition work as light as possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Save data that already exists in memory&lt;/li&gt;
&lt;li&gt;Write small JSON files&lt;/li&gt;
&lt;li&gt;Save only when dirty&lt;/li&gt;
&lt;li&gt;Treat network sync separately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doing huge JSON generation, compression, encryption, file writes, and cloud sync all at the last moment is risky.&lt;br&gt;
It is usually better to treat cloud or server saves as a separate system from local saves.&lt;/p&gt;
&lt;h2&gt;
  
  
  Write loading code as if failure is normal
&lt;/h2&gt;

&lt;p&gt;No matter how carefully you write saves, you cannot make corruption impossible.&lt;br&gt;
Crashes during writes, low storage, user deletion, migration bugs, and manual editing can all break files.&lt;br&gt;
So loading code should assume failure is normal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;TryLoad&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
&lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonUtility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromJson&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Failed to load json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\n&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;TryLoad&lt;/code&gt; only checks whether the file can be read as JSON.&lt;br&gt;
In a real load flow, treat the following as one pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check whether the file exists&lt;/li&gt;
&lt;li&gt;Parse JSON&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;version&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Migrate old versions&lt;/li&gt;
&lt;li&gt;Normalize and validate values&lt;/li&gt;
&lt;li&gt;Decide what to do with future versions or validation failures&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Even successfully loaded data may not be valid.&lt;br&gt;
A volume value could be &lt;code&gt;-999&lt;/code&gt;, a language code could be empty, the version could be missing, or an array could be unexpectedly huge.&lt;br&gt;
Normalize values after loading.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OptionSaveData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;masterVolume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clamp01&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;masterVolume&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bgmVolume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clamp01&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bgmVolume&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seVolume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clamp01&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seVolume&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;language&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ja"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is safer to treat save files almost like external input, even if your own app created them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a version field from the beginning
&lt;/h2&gt;

&lt;p&gt;I recommend adding a &lt;code&gt;version&lt;/code&gt; field to save data from the start.&lt;/p&gt;

&lt;p&gt;At first it feels unnecessary.&lt;br&gt;
But once the app is released and the save format changes, you will almost certainly want it.&lt;/p&gt;

&lt;p&gt;For example, an early version might have only one &lt;code&gt;volume&lt;/code&gt; field, and later you may want separate &lt;code&gt;masterVolume&lt;/code&gt;, &lt;code&gt;bgmVolume&lt;/code&gt;, and &lt;code&gt;seVolume&lt;/code&gt; fields.&lt;/p&gt;

&lt;p&gt;Without a version, you need to infer which format a file uses.&lt;br&gt;
Inference is possible, but annoying and error-prone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;CurrentSaveVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;OptionSaveData&lt;/span&gt; &lt;span class="nf"&gt;Migrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OptionSaveData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CurrentSaveVersion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Unsupported save version: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// v1 -&amp;gt; v2 migration&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also decide how to handle future versions.&lt;br&gt;
For example, if a user saves data with a newer app and then rolls back to an older app, the old app will see an unknown format.&lt;br&gt;
Immediately overwriting it with default data is dangerous.&lt;br&gt;
Depending on the project, you may stop loading and show a warning, read only compatible fields, or ask the user to update the app.&lt;/p&gt;

&lt;p&gt;Even if you throw an exception for future versions, connect that failure to backup handling, logging, user notification, or retry logic rather than blindly overwriting with defaults.&lt;/p&gt;

&lt;p&gt;For more complex save data, you may prepare old DTOs and migrate step by step.&lt;br&gt;
Even for option settings, having a version field from day one often helps later.&lt;/p&gt;
&lt;h2&gt;
  
  
  Split files by lifecycle
&lt;/h2&gt;

&lt;p&gt;Save data should not always be one huge JSON file.&lt;/p&gt;

&lt;p&gt;Option settings and game progress often have different lifecycles, so splitting them can make the system easier to manage.&lt;br&gt;
If game progress breaks, you do not want volume or language settings to break with it.&lt;br&gt;
If only options change frequently, you also avoid rewriting all progress data every time.&lt;/p&gt;

&lt;p&gt;A useful guideline is to split by lifecycle.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Contents&lt;/th&gt;
&lt;th&gt;Save frequency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;options.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Volume, language, vibration&lt;/td&gt;
&lt;td&gt;When settings change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;profile.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Name, avatar, basic profile settings&lt;/td&gt;
&lt;td&gt;When changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;progress.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stage progress, unlock state&lt;/td&gt;
&lt;td&gt;At milestones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cache_meta.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cache metadata&lt;/td&gt;
&lt;td&gt;When needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;But do not split too much.&lt;/p&gt;

&lt;p&gt;If two pieces of data must be updated together, keep them in the same file or design transaction-like handling.&lt;br&gt;
For example, if item consumption and stage unlock live in separate files and only one save succeeds, you may create inconsistent data.&lt;br&gt;
That is a sign the design needs another look.&lt;/p&gt;
&lt;h2&gt;
  
  
  Do not let multiple places save the same file
&lt;/h2&gt;

&lt;p&gt;A common save bug is allowing multiple classes to directly write the same file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. A saves newer data
2. B saves older data it had from earlier
3. A's change disappears
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a file I/O problem.&lt;br&gt;
It is a design problem.&lt;/p&gt;

&lt;p&gt;The fix is to centralize the save entry point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SaveRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;SaveData&lt;/span&gt; &lt;span class="n"&gt;_current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SaveData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;SaveInternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Unity, much of your logic may run on the main thread, so you may not always need a &lt;code&gt;lock&lt;/code&gt;.&lt;br&gt;
Still, centralizing "update save data and write it" is important.&lt;/p&gt;

&lt;p&gt;If save logic is scattered everywhere, it becomes hard to know what data is saved and when.&lt;br&gt;
This is often underestimated in small projects, but fixing it later is painful.&lt;/p&gt;
&lt;h2&gt;
  
  
  Use a dirty flag
&lt;/h2&gt;

&lt;p&gt;A dirty flag is useful for controlling save frequency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OptionStore&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;OptionSaveData&lt;/span&gt; &lt;span class="n"&gt;_data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_dirty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetMasterVolume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Approximately&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;masterVolume&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;masterVolume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_dirty&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SaveIfDirty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;_dirty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;_dirty&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;_dirty&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a dirty flag, callers can freely call &lt;code&gt;SaveIfDirty()&lt;/code&gt; during screen transitions or &lt;code&gt;OnApplicationPause&lt;/code&gt;.&lt;br&gt;
If nothing changed, nothing happens.&lt;br&gt;
The caller does not need to know the details.&lt;/p&gt;

&lt;p&gt;This is a minimal synchronous-save example.&lt;br&gt;
If you extend it to asynchronous saves, use an in-progress flag, revision number, or save queue so an old save completion does not clear a newer dirty state.&lt;/p&gt;

&lt;p&gt;Depending on platform and Unity version, &lt;code&gt;OnApplicationPause&lt;/code&gt; and &lt;code&gt;OnApplicationFocus&lt;/code&gt; may both be called or may occur close together.&lt;br&gt;
Make repeated save requests safe by combining dirty flags with centralized, serialized save entry points.&lt;/p&gt;

&lt;p&gt;One important detail: do not clear dirty when saving fails.&lt;br&gt;
If you clear it after a failure, memory may contain changed data while the file still contains old data.&lt;/p&gt;
&lt;h2&gt;
  
  
  Do not make saving too heavy
&lt;/h2&gt;

&lt;p&gt;For small JSON files, saving on the main thread is often fine.&lt;/p&gt;

&lt;p&gt;But as save data grows, the cost can become noticeable.&lt;/p&gt;

&lt;p&gt;Potentially expensive steps include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON string generation&lt;/li&gt;
&lt;li&gt;String allocation&lt;/li&gt;
&lt;li&gt;UTF-8 conversion&lt;/li&gt;
&lt;li&gt;Compression&lt;/li&gt;
&lt;li&gt;Encryption&lt;/li&gt;
&lt;li&gt;File writing&lt;/li&gt;
&lt;li&gt;Flush&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If too many of these happen at once, you may cause frame drops or a short freeze.&lt;/p&gt;

&lt;p&gt;Moving heavy work to a background thread can help.&lt;br&gt;
However, code that touches &lt;code&gt;UnityEngine.Object&lt;/code&gt; is generally expected to run on the main thread.&lt;br&gt;
First copy the required data into a save DTO, then decide how much of the remaining work can run off the main thread.&lt;/p&gt;

&lt;p&gt;The following is a minimal example that mainly moves file writing to a background thread.&lt;br&gt;
&lt;code&gt;JsonUtility.ToJson&lt;/code&gt; runs before &lt;code&gt;Task.Run&lt;/code&gt;, so this sample does not avoid frame drops caused by heavy JSON generation.&lt;br&gt;
If JSON generation itself is heavy, copy the data into a DTO that does not touch Unity APIs or &lt;code&gt;UnityEngine.Object&lt;/code&gt;, then consider moving JSON generation and file writing into the same background operation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Minimal explanatory sample.&lt;/span&gt;
&lt;span class="c1"&gt;// In this example, JsonUtility.ToJson runs on the main thread.&lt;/span&gt;
&lt;span class="c1"&gt;// In production, serialize save requests to the same file,&lt;/span&gt;
&lt;span class="c1"&gt;// or control the system so only the latest snapshot is committed.&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OptionSaveData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Clone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Copy to a save DTO that does not touch UnityEngine.Object&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonUtility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prettyPrint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SaveFiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OptionsPath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;SafeFileWriter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once saving becomes asynchronous, completion order can reverse.&lt;br&gt;
A newer save with volume 0.8 may finish first, then an older save with volume 0.5 may finish later and overwrite it.&lt;br&gt;
Avoid this by serializing saves, or by designing the system so only the latest snapshot is written.&lt;/p&gt;

&lt;p&gt;You do not have to make everything asynchronous from the start.&lt;br&gt;
For option settings, synchronous saving is often enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate encryption and tamper detection from durability
&lt;/h2&gt;

&lt;p&gt;When saving JSON, the contents are visible, so it is natural to consider encryption or tamper detection.&lt;br&gt;
But local encryption only makes casual inspection or editing harder.&lt;br&gt;
It is not complete anti-cheat.&lt;br&gt;
Important currency, paid items, rankings, and values that affect competitive play should be validated on the server side.&lt;/p&gt;

&lt;p&gt;Durability and anti-cheat are different goals.&lt;br&gt;
First prioritize save timing, corruption handling, recovery, versions, and normalization.&lt;br&gt;
Then add encryption or tamper detection if the project needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test cases to cover
&lt;/h2&gt;

&lt;p&gt;Save systems often look fine during normal play.&lt;br&gt;
You should intentionally break them during testing.&lt;/p&gt;

&lt;p&gt;At minimum, test these cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First launch with no save file&lt;/li&gt;
&lt;li&gt;Valid JSON can be loaded&lt;/li&gt;
&lt;li&gt;Empty file does not crash the app&lt;/li&gt;
&lt;li&gt;Broken JSON does not crash the app&lt;/li&gt;
&lt;li&gt;Old-version JSON can be loaded&lt;/li&gt;
&lt;li&gt;Strange values are normalized&lt;/li&gt;
&lt;li&gt;App can start even if &lt;code&gt;.tmp&lt;/code&gt; remains&lt;/li&gt;
&lt;li&gt;A valid-looking &lt;code&gt;.tmp&lt;/code&gt; is still discarded if that is the design&lt;/li&gt;
&lt;li&gt;If the main file is valid and an old &lt;code&gt;.bak&lt;/code&gt; remains, the main file is used&lt;/li&gt;
&lt;li&gt;Recovery from &lt;code&gt;.bak&lt;/code&gt; works&lt;/li&gt;
&lt;li&gt;App can start even if &lt;code&gt;.bak&lt;/code&gt; loads but rewriting the main file fails&lt;/li&gt;
&lt;li&gt;Repeated or simultaneous saves do not overwrite newer data with older data&lt;/li&gt;
&lt;li&gt;App can start after an exception during save&lt;/li&gt;
&lt;li&gt;If possible, test near-low-storage conditions&lt;/li&gt;
&lt;li&gt;On Android, test Home, app switching, and task kill&lt;/li&gt;
&lt;li&gt;Confirm data remains after app update&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Android in particular can behave differently depending on device and OS settings.&lt;br&gt;
Do not assume that working in the Editor is enough.&lt;/p&gt;

&lt;p&gt;Also decide what to do when saving fails.&lt;br&gt;
User notification, retry, returning to title, or server resync are gameplay and product decisions, not only engineering details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The first step of Unity save data is simple.&lt;br&gt;
For small settings, &lt;code&gt;PlayerPrefs&lt;/code&gt; works.&lt;br&gt;
For grouped data, &lt;code&gt;JsonUtility.ToJson&lt;/code&gt; plus &lt;code&gt;persistentDataPath&lt;/code&gt; can also work.&lt;/p&gt;

&lt;p&gt;But in a released game, issues appear: quit-time saves may not run, files may break during writes, failed loads may overwrite recoverable data with defaults, and old data may overwrite newer data.&lt;/p&gt;

&lt;p&gt;On mobile especially, do not rely too much on saving only when the app quits.&lt;/p&gt;

&lt;p&gt;A practical policy is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep &lt;code&gt;PlayerPrefs&lt;/code&gt; for small settings and simple flags&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;PlayerPrefs.Save()&lt;/code&gt; at confirmation points&lt;/li&gt;
&lt;li&gt;Save options and game progress at points where rollback would feel bad&lt;/li&gt;
&lt;li&gt;Avoid direct overwrite; replace via a temp file&lt;/li&gt;
&lt;li&gt;Prepare recovery from &lt;code&gt;.tmp&lt;/code&gt; and &lt;code&gt;.bak&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Do not overwrite immediately with defaults when loading fails&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;version&lt;/code&gt; and &lt;code&gt;Normalize&lt;/code&gt; to absorb old data and strange values&lt;/li&gt;
&lt;li&gt;Centralize save entry points and avoid parallel saves to the same file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Local saving is easy to overlook, but painful when it fails.&lt;br&gt;
Separate the range where &lt;code&gt;PlayerPrefs&lt;/code&gt; is enough from the range where JSON files are better, and design save timing, corruption handling, and recovery together.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Unity Scripting API: &lt;code&gt;PlayerPrefs.Save&lt;/code&gt;: &lt;a href="https://docs.unity3d.com/ScriptReference/PlayerPrefs.Save.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/ScriptReference/PlayerPrefs.Save.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unity Scripting API: &lt;code&gt;Application.persistentDataPath&lt;/code&gt;: &lt;a href="https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unity Scripting API: &lt;code&gt;JsonUtility&lt;/code&gt;: &lt;a href="https://docs.unity3d.com/ScriptReference/JsonUtility.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/ScriptReference/JsonUtility.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unity Scripting API: &lt;code&gt;Application.quitting&lt;/code&gt;: &lt;a href="https://docs.unity3d.com/ScriptReference/Application-quitting.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/ScriptReference/Application-quitting.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unity Scripting API: &lt;code&gt;MonoBehaviour.OnApplicationQuit&lt;/code&gt;: &lt;a href="https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnApplicationQuit.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnApplicationQuit.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unity Scripting API: &lt;code&gt;MonoBehaviour.OnApplicationPause&lt;/code&gt;: &lt;a href="https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnApplicationPause.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnApplicationPause.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unity Scripting API: &lt;code&gt;MonoBehaviour.OnApplicationFocus&lt;/code&gt;: &lt;a href="https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnApplicationFocus.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnApplicationFocus.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Android API reference: &lt;code&gt;AtomicFile&lt;/code&gt;: &lt;a href="https://developer.android.com/reference/android/util/AtomicFile" rel="noopener noreferrer"&gt;https://developer.android.com/reference/android/util/AtomicFile&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>android</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>R3 Operation Rules for Unity 6: SubscribeAwait, EveryUpdate, and Patterns to Avoid</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Wed, 08 Jul 2026 06:52:48 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/r3-operation-rules-for-unity-6-subscribeawait-everyupdate-and-patterns-to-avoid-1l63</link>
      <guid>https://dev.to/gamedevtoollab/r3-operation-rules-for-unity-6-subscribeawait-everyupdate-and-patterns-to-avoid-1l63</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the previous article, I covered the basic design rules for using R3 in Unity 6 projects: how to expose &lt;code&gt;ReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt; and &lt;code&gt;Subject&amp;lt;T&amp;gt;&lt;/code&gt;, how to manage subscription lifetimes, and how to separate R3 from &lt;code&gt;async/await&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This article continues from there and focuses on &lt;strong&gt;operation rules after introducing R3&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The target reader is a Unity team with around 10 programmers, where multiple people touch UI, networking, loading, presentation, and game flow. In solo development or short-lived prototypes, you may not need to be this strict. This article intentionally takes a conservative approach that prioritizes maintainability in long-running projects.&lt;/p&gt;

&lt;p&gt;This is not an article about Unity 6-specific APIs. It is about how to operate R3 safely in Unity 6-era projects. Most of the ideas also apply to other Unity versions supported by R3.&lt;/p&gt;

&lt;p&gt;The code samples omit common &lt;code&gt;using&lt;/code&gt; declarations such as &lt;code&gt;using R3;&lt;/code&gt;, &lt;code&gt;using UnityEngine;&lt;/code&gt;, TextMeshPro, R3.Unity uGUI extensions, and &lt;code&gt;CancellationToken&lt;/code&gt;. Methods such as &lt;code&gt;OnClickAsObservable()&lt;/code&gt; and &lt;code&gt;OnValueChangedAsObservable()&lt;/code&gt; assume R3.Unity uGUI extensions. In Unity projects, you need both the R3 core package and R3.Unity, and their versions should be aligned.&lt;/p&gt;

&lt;p&gt;This article assumes R3 1.2.0 or later. In R3 1.2.0, the default value of &lt;code&gt;cancelOnCompleted&lt;/code&gt; for &lt;code&gt;SubscribeAwait&lt;/code&gt;, &lt;code&gt;SelectAwait&lt;/code&gt;, &lt;code&gt;WhereAwait&lt;/code&gt;, and &lt;code&gt;ReactiveCommand&lt;/code&gt; was changed to &lt;code&gt;false&lt;/code&gt;. When combining &lt;code&gt;Take(1)&lt;/code&gt;, disposal, completion, and async work, distinguish between stopping further input and canceling the currently running operation.&lt;/p&gt;

&lt;p&gt;The three rules to take away are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SubscribeAwait(Drop)&lt;/code&gt; prevents duplicate execution while an operation is running; it is not the same as simultaneous-input protection or one-shot control.&lt;/li&gt;
&lt;li&gt;PlayerLoop/time-based streams such as &lt;code&gt;EveryUpdate&lt;/code&gt; and &lt;code&gt;Timer&lt;/code&gt; should make purpose, lifetime, and provider explicit.&lt;/li&gt;
&lt;li&gt;Use R3 for notification and wiring; keep domain logic and important mutual exclusion readable as ordinary C#.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Use SubscribeAwait for repeated-click protection
&lt;/h2&gt;

&lt;p&gt;A common UI problem is repeated clicks during a network request or loading operation.&lt;/p&gt;

&lt;p&gt;R3 provides &lt;code&gt;SubscribeAwait&lt;/code&gt; and &lt;code&gt;SelectAwait&lt;/code&gt;. They let you specify how to handle the next event while an async operation is still running by using &lt;code&gt;AwaitOperation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want to ignore clicks while a request is running, use &lt;code&gt;Drop&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_loginButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoginAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Drop&lt;/code&gt; discards &lt;strong&gt;subsequent events that arrive while the async operation is running&lt;/strong&gt;. It is useful for login buttons, purchase buttons, gacha buttons, and confirm buttons where duplicate execution during processing must be avoided.&lt;/p&gt;

&lt;p&gt;However, &lt;code&gt;Drop&lt;/code&gt; does not mean "execute only once forever." If the operation finishes quickly, the next click will be processed normally. If the dialog stays open after the operation, input can still be accepted again. If you want to accept only the first input for the whole screen or dialog session, use a separate mechanism such as &lt;code&gt;Take(1)&lt;/code&gt;, a confirmed flag, explicit disposal, or closing the screen.&lt;/p&gt;

&lt;p&gt;For a search box where you want to cancel the previous request and process only the latest text, &lt;code&gt;Switch&lt;/code&gt; is usually a better fit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_searchInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnValueChangedAsObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Use this when initial-value search is not needed.&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMilliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;UnityTimeProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;SearchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Switch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In R3.Unity, &lt;code&gt;OnValueChangedAsObservable()&lt;/code&gt; emits the latest value at the time of subscription. If initial-value search is part of the specification, that is fine. If you do not want to call an API with an empty string or initial value during screen initialization, add &lt;code&gt;Skip(1)&lt;/code&gt; or an explicit filter. The &lt;code&gt;Debounce&lt;/code&gt; provider also matters: should it run while the game is paused, or should it follow &lt;code&gt;Time.timeScale&lt;/code&gt;? In this example, &lt;code&gt;UnityTimeProvider.Update&lt;/code&gt; is specified explicitly.&lt;/p&gt;

&lt;p&gt;A simple team rule can start like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Recommended operation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Login, purchase, confirm, gacha&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Drop&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search, preview update&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Switch&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Must process in order&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Sequential&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safe to run in parallel&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Parallel&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For ordinary game UI, avoid using &lt;code&gt;Parallel&lt;/code&gt; casually. It is not common for multiple purchases, transitions, or network operations to be safe when executed at the same time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repeated-click protection and simultaneous-input protection are different
&lt;/h2&gt;

&lt;p&gt;Repeated-click protection and simultaneous-input protection are not the same thing.&lt;/p&gt;

&lt;p&gt;Repeated-click protection handles pressing the same button many times in a short period. Simultaneous-input protection handles multiple buttons, back keys, shortcuts, gamepad inputs, or other input paths firing at nearly the same time.&lt;/p&gt;

&lt;p&gt;If each button has its own &lt;code&gt;SubscribeAwait(Drop)&lt;/code&gt;, different buttons are not mutually exclusive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_okButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;DecideAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;_cancelButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;CancelAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this form, pressing &lt;code&gt;OK&lt;/code&gt; and &lt;code&gt;Cancel&lt;/code&gt; almost simultaneously can still run both operations. &lt;code&gt;Drop&lt;/code&gt; is applied per Observable stream.&lt;/p&gt;

&lt;p&gt;If operations should be mutually exclusive, first merge them into one event stream.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;DialogAction&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Cancel&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;_okButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DialogAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;_cancelButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DialogAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ExecuteDialogActionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;OK&lt;/code&gt; and &lt;code&gt;Cancel&lt;/code&gt; belong to the same mutual-exclusion group. If another event arrives while the operation is running, it will be dropped.&lt;/p&gt;

&lt;p&gt;Still, this is only duplicate-execution prevention while the operation is running. If a confirmation dialog must accept exactly one action and never accept another one afterward, use session-level one-shot control.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;_okButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DialogAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;_cancelButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DialogAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ExecuteDialogActionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cancelOnCompleted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Take(1)&lt;/code&gt; stops input after the first event. That is separate from whether the async operation already in progress should be canceled or allowed to finish. For purchases, payments, server state changes, and gacha confirmation, you must also design cancellation, partial success, retry behavior, and idempotency.&lt;/p&gt;

&lt;p&gt;Another common pattern is to keep an explicit confirmed/closed flag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_closed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="nf"&gt;ExecuteDialogActionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DialogAction&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_closed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;_closed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;SetButtonsInteractable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is also safer to put a shared gate on the processing side, not only in the UI stream. Input paths tend to grow over time: shortcuts, gamepads, keyboard back, test hooks, or direct method calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_isProcessing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="nf"&gt;ExecuteDialogActionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DialogAction&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_isProcessing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;_isProcessing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Set this at the very beginning of the entry point.&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;SetButtonsInteractable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;DialogAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;DecideAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;DialogAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;CancelAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_isProcessing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activeInHierarchy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;SetButtonsInteractable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;bool&lt;/code&gt; gate is a simple example for UI / Presenter code running on the Unity main thread. If the operation can be called from a service layer, background thread, SDK callback, or multiple threads, use an appropriate synchronization method such as &lt;code&gt;SemaphoreSlim&lt;/code&gt;, &lt;code&gt;Interlocked&lt;/code&gt;, &lt;code&gt;lock&lt;/code&gt;, or a dedicated job queue.&lt;/p&gt;

&lt;p&gt;Also be careful with &lt;code&gt;finally&lt;/code&gt;. Re-enabling buttons unconditionally can be wrong if the view was destroyed, the screen was closed, or the button was disabled for another reason. In production code, choose a policy such as restoring the previous interactable state, binding a processing state from the ViewModel, or not re-enabling UI when the screen closes.&lt;/p&gt;

&lt;p&gt;A useful way to separate responsibilities is this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;View&lt;/td&gt;
&lt;td&gt;Merge multiple input paths into one entry point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observable stream&lt;/td&gt;
&lt;td&gt;Drop later events while processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Presenter / ViewModel&lt;/td&gt;
&lt;td&gt;Hold shared gates and confirmed states&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service / Server&lt;/td&gt;
&lt;td&gt;Provide idempotency and duplicate-execution protection for important operations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A good team rule is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;UI operations that must not run together should not be handled by independent per-button subscriptions. Merge them as one mutual-exclusion group and pass them through one &lt;code&gt;SubscribeAwait(Drop)&lt;/code&gt; or one shared gate.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Do not overuse EveryUpdate
&lt;/h2&gt;

&lt;p&gt;R3 provides &lt;code&gt;Observable.EveryUpdate()&lt;/code&gt;. It is convenient because it creates a stream that emits every frame. But if everything becomes &lt;code&gt;EveryUpdate&lt;/code&gt;, the code can become harder to follow than a normal &lt;code&gt;Update&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;A bad example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EveryUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;Rotate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;CheckGround&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;UpdateAnimation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is little benefit in writing this with R3. A normal &lt;code&gt;Update&lt;/code&gt; is clearer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;Rotate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;CheckGround&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;UpdateAnimation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;EveryUpdate&lt;/code&gt; is useful when you want to treat frame updates as an event stream and compose it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EveryUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsGrounded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;OnLanded&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or when you want to observe something for a fixed number of frames:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EveryUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;UpdatePreview&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, if &lt;code&gt;UpdatePreview()&lt;/code&gt; performs heavy work such as rebuilding UI or generating meshes, running it for 60 consecutive frames may still be too expensive. This is not an R3-specific issue; be careful with the actual work performed every frame.&lt;/p&gt;

&lt;p&gt;A practical rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use normal &lt;code&gt;Update&lt;/code&gt; for ordinary game-loop logic.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;EveryUpdate&lt;/code&gt; only when frame events need stream composition.&lt;/li&gt;
&lt;li&gt;Long-lived &lt;code&gt;EveryUpdate&lt;/code&gt; subscriptions should have a clear reason.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EveryUpdate().Subscribe(...)&lt;/code&gt; with no meaningful composition should be questioned in review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need FixedUpdate-like timing, R3 can use &lt;code&gt;UnityFrameProvider.FixedUpdate&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EveryUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UnityFrameProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FixedUpdate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CheckFixedFrameEvent&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even so, physics logic itself is often clearer in a normal &lt;code&gt;FixedUpdate()&lt;/code&gt;. Use R3 here only when you need to compose events that happen on the FixedUpdate timing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Be explicit about TimeProvider and FrameProvider
&lt;/h2&gt;

&lt;p&gt;R3 can specify providers for time-based and frame-based operations. In Unity, the defaults are the Update-based providers: &lt;code&gt;UnityTimeProvider.Update&lt;/code&gt; and &lt;code&gt;UnityFrameProvider.Update&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;Timer&lt;/code&gt; or &lt;code&gt;Interval&lt;/code&gt; when waiting by time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;UnityTimeProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ShowMessage&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the operation should ignore &lt;code&gt;Time.timeScale&lt;/code&gt;, use &lt;code&gt;UpdateIgnoreTimeScale&lt;/code&gt; explicitly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;UnityTimeProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdateIgnoreTimeScale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ShowMessage&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to wait for a number of frames, use frame-based APIs instead of seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TimerFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UnityFrameProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ShowAfter60Frames&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Omitting the provider is not always wrong. But in games, whether a timer should stop during pause, ignore &lt;code&gt;timeScale&lt;/code&gt;, or be frame-based is part of the specification. For important code, specifying the provider makes review easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Use Observable Tracker for review and investigation
&lt;/h2&gt;

&lt;p&gt;R3 has Observable Tracker, a debugging aid for inspecting Observable and subscription state.&lt;/p&gt;

&lt;p&gt;When a team first introduces R3, common problems include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Subscriptions remain after closing a screen.&lt;/li&gt;
&lt;li&gt;Opening the same view increases the subscription count.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EveryUpdate&lt;/code&gt; keeps running longer than intended.&lt;/li&gt;
&lt;li&gt;Nobody knows which Observable is still alive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Observable Tracker is useful for investigating these problems.&lt;/p&gt;

&lt;p&gt;That said, it should not replace code review. The basic rule is to decide lifetimes in code first, then use Observable Tracker for suspicious areas or during the initial adoption phase.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do not let R3 leak too deeply into domain logic
&lt;/h2&gt;

&lt;p&gt;Once R3 is introduced, it can be tempting to turn everything into Observable streams. But if the core domain logic also becomes reactive, people who are not comfortable with R3 may struggle to read or modify it.&lt;/p&gt;

&lt;p&gt;For example, damage calculation is often just ordinary C#.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no need to express this calculation itself as an Observable chain.&lt;/p&gt;

&lt;p&gt;Use R3 to notify the result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_dead&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dead&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_dead&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_dead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In real code, the class that owns the &lt;code&gt;Subject&lt;/code&gt; should also dispose it.&lt;/p&gt;

&lt;p&gt;The basic policy is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep calculations, decisions, and state transitions as ordinary C#.&lt;/li&gt;
&lt;li&gt;Use R3 for state-change notifications, UI updates, and event wiring.&lt;/li&gt;
&lt;li&gt;Do not bury important specifications deep inside Observable chains.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters a lot in team development. If only the R3 expert can safely change the code, maintainability has already gone down.&lt;/p&gt;




&lt;h2&gt;
  
  
  Split complex chains
&lt;/h2&gt;

&lt;p&gt;Short R3 chains can be readable. Long ones become hard to review quickly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsVisible&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SortOrder&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;UpdateList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is still understandable, but real projects often add more conditions.&lt;/p&gt;

&lt;p&gt;When a chain grows, split it into named intermediate Observables or ordinary methods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;visibleItems&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FilterVisibleItems&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sortedItems&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;visibleItems&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SortItems&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;sortedItems&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateList&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;FilterVisibleItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsVisible&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SortItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SortOrder&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Named methods are easier to review than business rules hidden inside a long chain.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;ToReadOnlyReactiveProperty()&lt;/code&gt; when the result should be stored as state. Remember that the generated &lt;code&gt;ReadOnlyReactiveProperty&lt;/code&gt; also owns upstream subscriptions, so it needs a lifetime too.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do not overuse ToObservable
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ToObservable()&lt;/code&gt; is useful, but not everything should become an Observable.&lt;/p&gt;

&lt;p&gt;If you only need to calculate a total, LINQ is enough.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no need to write this as an Observable chain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;R3 is strong when values flow over time. If you are just aggregating an in-memory collection right now, LINQ or a normal &lt;code&gt;foreach&lt;/code&gt; is usually clearer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do not rely on AddTo(this) for every lifetime
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AddTo(this)&lt;/code&gt; is the basic way to tie a subscription to the lifetime of a &lt;code&gt;MonoBehaviour&lt;/code&gt; or &lt;code&gt;GameObject&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateHp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;AddTo(this)&lt;/code&gt; usually means lifetime until Destroy. It does not automatically mean disabled, hidden, tab-switched, popup-closed, or re-bound.&lt;/p&gt;

&lt;p&gt;If a tab creates subscriptions every time it is selected, use a separate &lt;code&gt;DisposableBag&lt;/code&gt; for that tab binding.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;DisposableBag&lt;/span&gt; &lt;span class="n"&gt;_tabDisposables&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;BindTab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TabModel&lt;/span&gt; &lt;span class="n"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_tabDisposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_tabDisposables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateItems&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;_tabDisposables&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnDestroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_tabDisposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a disposed &lt;code&gt;DisposableBag&lt;/code&gt; receives a new disposable, the new one is disposed immediately. Reset it with &lt;code&gt;= default&lt;/code&gt; before reusing the field.&lt;/p&gt;

&lt;p&gt;The same applies to Views whose &lt;code&gt;Initialize()&lt;/code&gt; method may be called multiple times.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;DisposableBag&lt;/span&gt; &lt;span class="n"&gt;_bindDisposables&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_bindDisposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_bindDisposables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateHp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;_bindDisposables&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a prefab is reused, dependency injection runs again, or a tab is rebound, &lt;code&gt;AddTo(this)&lt;/code&gt; alone can leave the previous subscription alive. Distinguish between GameObject lifetime, display lifetime, and binding lifetime.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to think about OnErrorResume
&lt;/h2&gt;

&lt;p&gt;R3 does not use the traditional Rx model where &lt;code&gt;OnError&lt;/code&gt; stops the subscription. Instead, errors are handled through &lt;code&gt;OnErrorResume&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For UI events, this is often convenient: a failed click should not necessarily kill the button subscription forever. But it also does not mean exceptions can be swallowed silently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_loginButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoginAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;OperationCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activeInHierarchy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;_dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Login failed."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second argument is R3's &lt;code&gt;OnErrorResume&lt;/code&gt; handler, not the old Rx &lt;code&gt;OnError&lt;/code&gt;. The subscription does not stop.&lt;/p&gt;

&lt;p&gt;In production code, decide these rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send logs to &lt;code&gt;Debug.LogException&lt;/code&gt; or the project's standard logger.&lt;/li&gt;
&lt;li&gt;Separate user-facing failure messages from developer logs.&lt;/li&gt;
&lt;li&gt;Decide whether &lt;code&gt;OperationCanceledException&lt;/code&gt; should be shown to users.&lt;/li&gt;
&lt;li&gt;Do not touch a destroyed View after screen transition.&lt;/li&gt;
&lt;li&gt;For important operations, consider representing success/failure as a return value instead of relying only on Observable error handling.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Patterns to avoid
&lt;/h2&gt;

&lt;p&gt;These are the patterns I would explicitly ban in team development.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern to avoid&lt;/th&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Alternative&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;public ReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anyone can write to it&lt;/td&gt;
&lt;td&gt;Keep it private and expose &lt;code&gt;ReadOnlyReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;public Subject&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anyone can call &lt;code&gt;OnNext&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Keep it private and expose &lt;code&gt;Observable&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;static/global &lt;code&gt;Subject&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Publisher, subscriber, and lifetime become unclear&lt;/td&gt;
&lt;td&gt;Keep events within a feature or screen scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Subscribe&lt;/code&gt; without lifetime&lt;/td&gt;
&lt;td&gt;Leaks and duplicate execution&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;AddTo&lt;/code&gt; / &lt;code&gt;DisposableBag&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adding subscriptions every &lt;code&gt;Initialize()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Re-initialization creates duplicates&lt;/td&gt;
&lt;td&gt;Dispose binding disposables before rebinding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-button &lt;code&gt;Drop&lt;/code&gt; for exclusive actions&lt;/td&gt;
&lt;td&gt;Does not prevent simultaneous input&lt;/td&gt;
&lt;td&gt;Merge by mutual-exclusion group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Using &lt;code&gt;EveryUpdate&lt;/code&gt; as &lt;code&gt;Update&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Intent becomes harder to follow&lt;/td&gt;
&lt;td&gt;Use normal &lt;code&gt;Update&lt;/code&gt; for ordinary loops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Making everything &lt;code&gt;ToObservable()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Harder than LINQ or &lt;code&gt;foreach&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Use Observable only where time flow matters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Very long chains&lt;/td&gt;
&lt;td&gt;Hard to review&lt;/td&gt;
&lt;td&gt;Split into intermediate Observables or methods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swallowing errors in &lt;code&gt;OnErrorResume&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Failures disappear from monitoring&lt;/td&gt;
&lt;td&gt;Define logging and display rules&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Even when exposing &lt;code&gt;ReadOnlyReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt; or &lt;code&gt;Observable&amp;lt;T&amp;gt;&lt;/code&gt;, a caller who knows the concrete object could force an unsafe cast. The point is not to make abuse impossible through types alone. The point is to hide write access from the public API and make unsafe casts a team-rule violation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Team operation rules
&lt;/h2&gt;

&lt;p&gt;Based on the table above, code review can focus on four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the write entry point hidden?&lt;/li&gt;
&lt;li&gt;Is the subscription lifetime clear, including re-initialization?&lt;/li&gt;
&lt;li&gt;Are repeated clicks, simultaneous input, and one-shot control treated as separate problems?&lt;/li&gt;
&lt;li&gt;Is the code using Observable where ordinary C#, LINQ, &lt;code&gt;Update&lt;/code&gt;, or &lt;code&gt;async/await&lt;/code&gt; would be clearer?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not start by creating too many detailed rules. It is safer to first establish private write access, lifetime management, input exclusivity, and limited &lt;code&gt;EveryUpdate&lt;/code&gt; usage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adoption scope
&lt;/h2&gt;

&lt;p&gt;R3 is safer to introduce gradually, not all at once across the entire project.&lt;/p&gt;

&lt;p&gt;A practical adoption order is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;UI events and repeated-click protection&lt;/li&gt;
&lt;li&gt;ViewModel-to-View state notification&lt;/li&gt;
&lt;li&gt;Loading progress and busy flags&lt;/li&gt;
&lt;li&gt;Composition of multiple inputs&lt;/li&gt;
&lt;li&gt;Game events only where needed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you try to convert every Model, Service, and event to R3 at once, review cost will rise quickly. In a team with R3 beginners, starting from UI and ViewModel code usually leads to smoother adoption.&lt;/p&gt;




&lt;h2&gt;
  
  
  Notes for migrating from UniRx
&lt;/h2&gt;

&lt;p&gt;If your team has UniRx experience, people may treat R3 as just a newer UniRx. But several design details are different.&lt;/p&gt;

&lt;p&gt;Important points include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;Debounce&lt;/code&gt;, not &lt;code&gt;Throttle&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Do not assume &lt;code&gt;OnError&lt;/code&gt; stops the subscription.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;SubscribeAwait&lt;/code&gt; and &lt;code&gt;AwaitOperation&lt;/code&gt; for async UI events.&lt;/li&gt;
&lt;li&gt;Follow R3's disposable patterns such as &lt;code&gt;DisposableBag&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Be explicit about Unity providers where timing matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Migration is not only API replacement. Revisit error handling, async behavior, and subscription lifetime design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recommended practical structure
&lt;/h2&gt;

&lt;p&gt;A typical maintainable structure is to separate Model, Presenter/ViewModel, and View responsibilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model
&lt;/h3&gt;

&lt;p&gt;The Model owns state and publishes change notifications. It does not expose write access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerStatus&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDisposable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_hp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ReadOnlyReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_hp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;_hp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_hp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_hp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ViewModel / Presenter
&lt;/h3&gt;

&lt;p&gt;The ViewModel or Presenter converts Model state into UI-friendly state. Any generated &lt;code&gt;ReadOnlyReactiveProperty&lt;/code&gt; must also have a lifetime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;DisposableBag&lt;/span&gt; &lt;span class="n"&gt;_disposables&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;ReadOnlyReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_canDecide&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ReadOnlyReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CanDecide&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_canDecide&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Wallet&lt;/span&gt; &lt;span class="n"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_disposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_disposables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;_canDecide&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CombineLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Gold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;hp&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToReadOnlyReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;_canDecide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;_disposables&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_disposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;Initialize()&lt;/code&gt; may be called more than once, rebuild subscriptions after disposing the previous ones. If the class is single-initialization only, make that assumption explicit in the design.&lt;/p&gt;

&lt;h3&gt;
  
  
  View
&lt;/h3&gt;

&lt;p&gt;The View sends UI events to the Presenter and reflects Presenter state into UI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;DisposableBag&lt;/span&gt; &lt;span class="n"&gt;_bindDisposables&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerPresenter&lt;/span&gt; &lt;span class="n"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_bindDisposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_bindDisposables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CanDecide&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canDecide&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_decideButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interactable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;canDecide&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;_bindDisposables&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;_decideButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DecideAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;_bindDisposables&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnDestroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_bindDisposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;Initialize()&lt;/code&gt; is guaranteed to be called only once, &lt;code&gt;AddTo(this)&lt;/code&gt; can be enough. If the View can be rebound or re-initialized, explicitly dispose the previous binding subscriptions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing not to use R3 is also important
&lt;/h2&gt;

&lt;p&gt;R3 is useful, but not every piece of code needs it.&lt;/p&gt;

&lt;p&gt;Examples where R3 is often unnecessary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple sequential logic&lt;/li&gt;
&lt;li&gt;One-time loading&lt;/li&gt;
&lt;li&gt;Complex damage calculation&lt;/li&gt;
&lt;li&gt;AI decision-making core&lt;/li&gt;
&lt;li&gt;Core physics update logic&lt;/li&gt;
&lt;li&gt;One-off collection aggregation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are often clearer with ordinary C#, &lt;code&gt;async/await&lt;/code&gt;, &lt;code&gt;Update&lt;/code&gt;, or LINQ.&lt;/p&gt;

&lt;p&gt;Use R3 where you need to compose events or value flows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI events&lt;/li&gt;
&lt;li&gt;State-change notifications&lt;/li&gt;
&lt;li&gt;Composition of multiple inputs&lt;/li&gt;
&lt;li&gt;Repeated-click and simultaneous-input protection&lt;/li&gt;
&lt;li&gt;Loading progress&lt;/li&gt;
&lt;li&gt;ViewModel-to-View notification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The purpose of R3 is not to make code look advanced. It is to make event flow and lifetime explicit enough for the team to maintain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;R3 is very effective when its usage is constrained. It works well for UI events, ViewModel-to-View notifications, loading progress, busy flags, and input composition. In those areas, it can express intent more clearly than hand-written event management.&lt;/p&gt;

&lt;p&gt;But if the team uses it freely everywhere, &lt;code&gt;Subject&lt;/code&gt;, &lt;code&gt;EveryUpdate&lt;/code&gt;, long chains, and subscriptions with unclear lifetimes will multiply.&lt;/p&gt;

&lt;p&gt;The most important point in this article is to separate repeated clicks, simultaneous input, one-shot control, subscription lifetime, and time providers. For a 10-person Unity team that needs to maintain a project over time, R3 should not be introduced as a tool for making everything Observable. It should be introduced as a &lt;strong&gt;restricted set of rules for writing notification and wiring code safely&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>gamedev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
