<?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>Practical UE5 File Loading Optimization: Beyond Parallel I/O and Raising the FPS Cap</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Mon, 24 Aug 2026 14:15:26 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/practical-ue5-file-loading-optimization-beyond-parallel-io-and-raising-the-fps-cap-5g6n</link>
      <guid>https://dev.to/gamedevtoollab/practical-ue5-file-loading-optimization-beyond-parallel-io-and-raising-the-fps-cap-5g6n</guid>
      <description>&lt;p&gt;UE5 loading optimization is often reduced to two ideas: read more files in parallel, and raise the FPS cap during a loading screen. Both can help, but only when they target the real bottleneck.&lt;/p&gt;

&lt;p&gt;A transition also includes dependency discovery, decompression, UObject creation, &lt;code&gt;Serialize&lt;/code&gt; and &lt;code&gt;PostLoad&lt;/code&gt;, activation, collision, texture/audio streaming, PSOs, and GC. If activation dominates the Game Thread, more parallel reads may only create a larger completion spike.&lt;/p&gt;

&lt;p&gt;This article targets UE 5.8 as of August 2026. Use this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Measure I/O and post-load work separately.&lt;/li&gt;
&lt;li&gt;Reduce hard-reference graphs and requested data.&lt;/li&gt;
&lt;li&gt;Preload by player-experience phase.&lt;/li&gt;
&lt;li&gt;Change loading budgets during controlled transitions instead of blindly unlocking FPS.&lt;/li&gt;
&lt;li&gt;Tune containers, compression, streaming, and activation on target hardware.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Read by symptom&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slow startup: reference graphs, Asset Manager, IoStore.&lt;/li&gt;
&lt;li&gt;Teleport hitch: World Partition sources, texture mips, PSOs.&lt;/li&gt;
&lt;li&gt;Freeze after the loading screen: activation, overlap, PSOs.&lt;/li&gt;
&lt;li&gt;Slow custom JSON or binary data: split Read, Decode, and Apply.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Decompose load time first
&lt;/h2&gt;

&lt;p&gt;Treat loading as a pipeline:&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;Typical work&lt;/th&gt;
&lt;th&gt;Common bottleneck&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Discover&lt;/td&gt;
&lt;td&gt;Asset Registry, Primary Assets, dependencies&lt;/td&gt;
&lt;td&gt;Large search scope or graph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read&lt;/td&gt;
&lt;td&gt;Container or file reads&lt;/td&gt;
&lt;td&gt;Random I/O, many small requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decode&lt;/td&gt;
&lt;td&gt;Decryption, Oodle decompression&lt;/td&gt;
&lt;td&gt;CPU saturation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deserialize&lt;/td&gt;
&lt;td&gt;UObject creation, &lt;code&gt;Serialize&lt;/code&gt;, &lt;code&gt;PostLoad&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Custom work, nested sync loads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Activate&lt;/td&gt;
&lt;td&gt;Actor spawn, Component registration, render setup&lt;/td&gt;
&lt;td&gt;Game Thread, collision, PSOs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If Read takes 0.4 seconds and Activate takes 2.0 seconds, more I/O concurrency cannot remove most of the wait. It may make more packages finish together and worsen the activation spike.&lt;/p&gt;

&lt;p&gt;Record median and p95 transition time, worst Game Thread frame, bytes and request count, decode CPU, activation time, and peak memory. Separate cold and warm-cache runs. Use cooked packaged builds on target devices for final decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate loading from waiting with Unreal Insights
&lt;/h2&gt;

&lt;p&gt;In UE 5.8, &lt;code&gt;AssetLoadTime&lt;/code&gt; exposes timing around asset serialization, while &lt;code&gt;LoadTime&lt;/code&gt; covers runtime loading from pak or IoStore paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-trace=default,AssetLoadTime,LoadTime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use Editor or PIE for reference investigation, then compare a Shipping-equivalent packaged build with the same channels, device, and cache conditions. Add &lt;code&gt;-statnamedevents&lt;/code&gt; only when Blueprint names are necessary, and place bookmarks around the transition.&lt;/p&gt;

&lt;p&gt;Check for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I/O or Async Loading Thread starvation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PostLoad&lt;/code&gt;, Actor creation, or registration spikes.&lt;/li&gt;
&lt;li&gt;Duplicate requests or urgent work buried under broad preloads.&lt;/li&gt;
&lt;li&gt;Texture or PSO hitches after package loading completes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Focused Zen Loader logging can help:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-LogCmds="LogStreaming veryverbose"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not keep VeryVerbose logging in normal profiling builds.&lt;/p&gt;

&lt;p&gt;Avoid polling &lt;code&gt;GetAsyncLoadPercentage&lt;/code&gt; every frame. Epic warns that it is slow and may block async loading. Prefer load groups, handle callbacks, and monotonic phase-based progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  The largest optimization is not loading the asset
&lt;/h2&gt;

&lt;p&gt;Hard references such as &lt;code&gt;UTexture2D*&lt;/code&gt;, &lt;code&gt;USkeletalMesh*&lt;/code&gt;, and Blueprint classes pull dependency graphs into memory. A persistent “all characters” registry can make the title screen load meshes, materials, animation, audio, and VFX. Inspect inclusive size, dependency direction, and cooked chunks with Reference Viewer, Size Map, and Asset Audit.&lt;/p&gt;

&lt;p&gt;When owner and target lifetimes differ, use soft references:&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;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EditDefaultsOnly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;TSoftObjectPtr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;USkeletalMesh&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CharacterMesh&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;EditDefaultsOnly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;TSoftClassPtr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AActor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CharacterClass&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A soft reference does not load its target automatically. Request it through Streamable Manager or Asset Manager when its phase begins.&lt;/p&gt;

&lt;p&gt;Changing the type does &lt;strong&gt;not&lt;/strong&gt; guarantee cooking. The Cooker still needs a discoverable route through a serialized &lt;code&gt;UPROPERTY&lt;/code&gt;, Asset Bundle, Primary Asset rule, &lt;code&gt;PrimaryAssetLabel&lt;/code&gt;, explicit Cook Rule, or similar mechanism. Runtime-generated soft paths need separate cook registration. Verify final chunk membership in a packaged build.&lt;/p&gt;

&lt;p&gt;Keep hard references when assets are always required together. Otherwise inspect &lt;code&gt;ConstructorHelpers&lt;/code&gt;, Blueprint types and casts, class defaults, and distant World Partition Actor links. Interfaces, IDs, Gameplay Tags, events, or subsystem queries can avoid loading a concrete class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Load by experience phase with Asset Manager
&lt;/h2&gt;

&lt;p&gt;Individual soft references become difficult to own and release consistently. Centralize policy around Asset Manager and divide Secondary Assets into named Asset Bundles:&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;UCLASS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BlueprintType&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;UCharacterDefinition&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;UPrimaryDataAsset&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;public:&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;EditDefaultsOnly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AssetBundles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Menu"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;TSoftObjectPtr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UTexture2D&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Portrait&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;EditDefaultsOnly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AssetBundles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Gameplay"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;TSoftObjectPtr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;USkeletalMesh&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Mesh&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;EditDefaultsOnly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AssetBundles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Gameplay"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;TSoftClassPtr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AActor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CharacterClass&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;EditDefaultsOnly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AssetBundles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Voice"&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;TSoftObjectPtr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;USoundBase&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;VoiceAssets&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;Load &lt;code&gt;Menu&lt;/code&gt; during selection, &lt;code&gt;Gameplay&lt;/code&gt; after confirmation, and &lt;code&gt;Voice&lt;/code&gt; before dialogue. Bundle by &lt;strong&gt;player-experience phase&lt;/strong&gt;, not only file type.&lt;/p&gt;

&lt;p&gt;This UE 5.8 excerpt shows ownership and request replacement for one character. It omits declarations, logging, retries, and multi-consumer counting; older versions may use earlier overloads.&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="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;UStageLoadSubsystem&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;LoadCharacter&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;FPrimaryAssetId&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Id&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;LoadedCharacterId&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;ReleaseCharacter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;LoadedCharacterId&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="n"&gt;FAssetManagerLoadParams&lt;/span&gt; &lt;span class="n"&gt;Params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FStreamableManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;DefaultAsyncLoadPriority&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnComplete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FStreamableDelegateWithHandle&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CreateWeakLambda&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;this&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;TSharedPtr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FStreamableHandle&lt;/span&gt;&lt;span class="o"&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LoadedCharacterId&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="k"&gt;return&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;UObject&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Asset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UAssetManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetPrimaryAssetObject&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="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;OnCharacterReady&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="k"&gt;const&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;FName&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Bundles&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;FName&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;"Gameplay"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;CharacterHandle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UAssetManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;LoadPrimaryAsset&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;Bundles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MoveTemp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Params&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;UStageLoadSubsystem&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ReleaseCharacter&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;CharacterHandle&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;CharacterHandle&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;HasLoadCompleted&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CharacterHandle&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;CancelHandle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;CharacterHandle&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="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;LoadedCharacterId&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;UAssetManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;UnloadPrimaryAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LoadedCharacterId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;LoadedCharacterId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FPrimaryAssetId&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;Production code still needs request generations, consumer counts, and separate failure and cancellation signals.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Lifetime model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LoadPrimaryAsset(s)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retained until &lt;code&gt;UnloadPrimaryAsset(s)&lt;/code&gt;. The handle controls progress, waiting, cancellation, and callbacks, not asset lifetime.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PreloadPrimaryAssets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Valid while its handle is retained; releasable afterward if unreferenced.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct Streamable Manager request&lt;/td&gt;
&lt;td&gt;A retained handle or hard reference owns the useful lifetime.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;LoadPrimaryAsset(s)&lt;/code&gt; may return a null handle when no new work is required while still invoking completion. Validate the object in &lt;code&gt;OnComplete&lt;/code&gt;, keep failure/cancellation/replacement distinct, and put release under one owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel loading is not “the more, the faster”
&lt;/h2&gt;

&lt;p&gt;Unlimited concurrency can create small random reads, decode contention, registration bursts, high peak memory, and urgent-work starvation. Group requests by player need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Minimum: map, player, required UI.&lt;/li&gt;
&lt;li&gt;Immediate: nearby assets, first enemies, essential SFX.&lt;/li&gt;
&lt;li&gt;Later: next area, additional enemies, dialogue voice.&lt;/li&gt;
&lt;li&gt;Optional: distant high mips and cosmetics.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only the minimum set should receive high priority. Share in-flight handles and track consumers to prevent duplicate requests and premature unloads.&lt;/p&gt;

&lt;p&gt;Cancellation does not rewind completed work. Treat request cancellation, handle release, and UObject-reference removal as separate operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not represent readiness with one boolean
&lt;/h2&gt;

&lt;p&gt;An async callback rarely means gameplay is safe to start. Track at least:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Requested&lt;/li&gt;
&lt;li&gt;AssetsReady&lt;/li&gt;
&lt;li&gt;WorldReady&lt;/li&gt;
&lt;li&gt;RuntimeReady&lt;/li&gt;
&lt;li&gt;PresentationReady&lt;/li&gt;
&lt;li&gt;Interactive&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After a handle completes, spawning, collision, UI, network synchronization, texture/audio readiness, and PSOs may remain. Aggregate completion from the owning subsystems.&lt;/p&gt;

&lt;p&gt;Use measured phase weights for monotonic display progress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AssetsReady       35%
WorldReady        25%
RuntimeReady      20%
PresentationReady 15%
Interactive        5%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep display progress separate from the internal state machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can a higher FPS cap make loading faster?
&lt;/h2&gt;

&lt;p&gt;Sometimes. Streaming and Actor/Component registration include per-frame budgeted work. Raising the FPS cap may apply those budgets more often per second.&lt;/p&gt;

&lt;p&gt;It will not help when storage, decompression CPU, or one long Game Thread task is saturated. It may hurt when rendering, animation, UI, or unrelated Tick consumes the extra frames. Treat &lt;code&gt;t.MaxFPS 0&lt;/code&gt; as a diagnostic experiment, not a permanent strategy.&lt;/p&gt;

&lt;p&gt;During gameplay, stable frame time matters. During a controlled transition, total completion time may matter more:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Example setting&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;s.AsyncLoadingTimeLimit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-frame time for async loading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s.PriorityAsyncLoadingExtraTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extra time for priority loading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s.LevelStreamingActorsUpdateTimeLimit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Time for streamed Actor updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s.PriorityLevelStreamingActorsUpdateExtraTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extra priority streaming time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s.LevelStreamingComponentsRegistrationGranularity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Registration batch size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s.UnregisterComponentsTimeLimit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Time for unregister work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Names and behavior vary by version and platform. Check &lt;code&gt;VariableName ?&lt;/code&gt;, Engine Source, Device Profiles, and current values. Centralize changes and restore prior values on success, cancellation, error, or map transition. Use &lt;strong&gt;Gameplay&lt;/strong&gt;, &lt;strong&gt;Transition&lt;/strong&gt;, and &lt;strong&gt;Tail&lt;/strong&gt; profiles; wait only for the mandatory remainder in Tail.&lt;/p&gt;

&lt;p&gt;Keep the loading screen cheap: heavy 3D scenes, Niagara, blur, Scene Capture, animated UI, and unnecessary Tick compete with loading. Preserve required render preparation or the hitch moves to the first interactive frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid routine synchronous flushes
&lt;/h2&gt;

&lt;p&gt;Calling &lt;code&gt;FlushAsyncLoading&lt;/code&gt; immediately after an async request recreates the Game Thread stall it was meant to avoid. It is easier to justify at startup, in a fully non-interactive transition, or in deterministic editor and automation workflows.&lt;/p&gt;

&lt;p&gt;If a synchronous boundary is unavoidable, start early, keep the mandatory set small, and wait only for the remaining tail. &lt;code&gt;LoadSynchronous&lt;/code&gt; is not safe merely because the directly referenced asset looks small; its hard-reference graph may be large.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tune IoStore, Zen Loader, and Oodle together
&lt;/h2&gt;

&lt;p&gt;Zen Loader uses cooked dependency data and retrieves chunks from &lt;code&gt;.utoc&lt;/code&gt; and &lt;code&gt;.ucas&lt;/code&gt; containers. IoStore is the normal UE 5.8 packaged path.&lt;/p&gt;

&lt;p&gt;Use Asset Registry for packaged discovery. Place custom data deliberately as a UE asset, staged UFS file, Non-UFS file, or Save/Download data; packaged Content is not a loose-file directory.&lt;/p&gt;

&lt;p&gt;Inspect final layout when chunk placement looks wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UnrealPak.exe IoStore -Describe=&amp;lt;Global.ucas&amp;gt; -DumpToFile=Output.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Encrypted containers also need &lt;code&gt;-CryptoKeys=&amp;lt;Crypto.json&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Compression trades storage reads for decode CPU. Oodle &lt;strong&gt;Method&lt;/strong&gt; changes the main size/decode-speed tradeoff; &lt;strong&gt;Level&lt;/strong&gt; changes encoder effort and output size without selecting another runtime decoder. Smaller output can still reduce reads and patch size.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;General tendency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kraken&lt;/td&gt;
&lt;td&gt;Strong compression and good decode speed; useful baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mermaid&lt;/td&gt;
&lt;td&gt;Less compression, faster decode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Selkie&lt;/td&gt;
&lt;td&gt;Favors decode speed further&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leviathan&lt;/td&gt;
&lt;td&gt;Smaller data is possible, with slower decode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Compare total time, decode CPU, container size, bytes read, and peak memory on weak-CPU and weak-storage targets. No compression may remove decode work while increasing I/O, download, and patch size.&lt;/p&gt;

&lt;h2&gt;
  
  
  World Partition is not only Cell Size
&lt;/h2&gt;

&lt;p&gt;Small cells increase request count; large cells include unnecessary Actors. Tune for movement speed, visibility, Actor density, and storage. Before adding Runtime Grids, use HLOD Layers, Data Layers, and spatial loading.&lt;/p&gt;

&lt;p&gt;For teleports, create a World Partition Streaming Source at the destination before moving the player:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure grid, priority, and target state.&lt;/li&gt;
&lt;li&gt;Load during a fade, door animation, or elevator ride.&lt;/li&gt;
&lt;li&gt;Move after streaming completion.&lt;/li&gt;
&lt;li&gt;Disable the temporary source and release the old area.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Direct references between distant Actors can couple their cells. Replace them with IDs, events, or subsystem queries where appropriate.&lt;/p&gt;

&lt;p&gt;Increasing &lt;code&gt;wp.Runtime.MaxLoadingLevelStreamingCells&lt;/code&gt; may shorten elapsed time while raising CPU, memory, and registration peaks. Measure gameplay and transitions separately. UE 5.8 includes World Partition Insights; use branch-appropriate tools on older versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimize activation and second-stage resources
&lt;/h2&gt;

&lt;p&gt;A completed read can still be followed by expensive spawning, registration, collision, and overlap work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consider ISM/HISM for repeated static geometry.&lt;/li&gt;
&lt;li&gt;Avoid excessive Actor counts for non-gameplay decoration.&lt;/li&gt;
&lt;li&gt;Remove always-present Components that are rarely needed.&lt;/li&gt;
&lt;li&gt;Avoid global Actor searches and synchronous loads in &lt;code&gt;BeginPlay&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Delay nonessential initialization until later frames or proximity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Initial overlap calculation can be expensive. Evaluate &lt;code&gt;UpdateOverlapsMethodDuringLevelStreaming&lt;/code&gt; and “Generate Overlap Events During Level Streaming” per class: decoration may not need it, while triggers may depend on it.&lt;/p&gt;

&lt;p&gt;A package can be loaded while presentation data is still missing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Textures:&lt;/strong&gt; prioritize critical UI and near-field mips; do not mark everything &lt;code&gt;Never Stream&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio:&lt;/strong&gt; stream long assets, but prepare the first chunk for sounds that must start immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shaders/PSOs:&lt;/strong&gt; use PSO Precaching or Pipeline Cache for materials guaranteed to appear at startup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prepare mandatory presentation data and continue optional resources in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom files need Read, Decode, and Apply stages
&lt;/h2&gt;

&lt;p&gt;Many worker tasks using synchronous file APIs can still create competing small reads. Choose storage first, then the API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small text: &lt;code&gt;FFileHelper::LoadFileToStringAsync&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Large sequential processing without one full allocation: &lt;code&gt;FFileHelper::LoadFileInBlocks&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Async offset-and-size reads: &lt;code&gt;IAsyncReadFileHandle::ReadRequest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Custom cooked chunks: consider &lt;code&gt;FIoDispatcher&lt;/code&gt; with Asset Registry and chunk rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;LoadFileInBlocks&lt;/code&gt; calls a visitor for sequential blocks. It avoids retaining the whole file, but does &lt;strong&gt;not&lt;/strong&gt; guarantee asynchronous I/O. Keep it off the Game Thread. Use &lt;code&gt;IAsyncReadFileHandle::ReadRequest&lt;/code&gt; for true async range reads.&lt;/p&gt;

&lt;p&gt;Validate size and read requests; a non-null &lt;code&gt;OpenAsyncRead&lt;/code&gt; handle is not full success. Keep each &lt;code&gt;IAsyncReadRequest&lt;/code&gt; alive until completion and define one owner for the handle, request, and buffer.&lt;/p&gt;

&lt;p&gt;Use this pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read:&lt;/strong&gt; obtain bytes with async I/O.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decode:&lt;/strong&gt; decompress, validate, and parse on a worker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply:&lt;/strong&gt; create or update necessary UObjects on the Game Thread.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep parsed data in plain structures until Apply. Avoid retaining compressed input, decompressed output, and final objects simultaneously. For random access, index the format and read only required ranges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefetch from player intent and control memory overlap
&lt;/h2&gt;

&lt;p&gt;Distance-only prefetch is unreliable with fast movement and branching paths. Strong signals include a confirmed stage, destination, character selection, door sequence, matchmaking result, quest update, or server instruction.&lt;/p&gt;

&lt;p&gt;Scale requests to confidence: hover may load a portrait, confirmation loads Gameplay, and an irreversible transition loads voice. Track hit rate, time hidden, wasted bytes, and residency because wrong predictions still consume work.&lt;/p&gt;

&lt;p&gt;Preloading the next stage while retaining the old one can overlap working sets and trigger GC or OS pressure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prefetch the new minimum set while the old scene remains visible.&lt;/li&gt;
&lt;li&gt;Stop expensive old-scene systems after the fade begins.&lt;/li&gt;
&lt;li&gt;Release old handles and references.&lt;/li&gt;
&lt;li&gt;Place necessary GC in a known non-interactive interval.&lt;/li&gt;
&lt;li&gt;Activate the new scene and continue optional loading.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use Memory Insights before adding more manual GC. Compare peak memory beside total time when tuning concurrency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fast Geometry Streaming is not the default answer
&lt;/h2&gt;

&lt;p&gt;UE 5.8 improves the Experimental Fast Geometry Streaming plugin for static, non-gameplay assets. It may help very large static worlds, but does not replace reference cleanup, Asset Manager, World Partition, or gameplay Actor loading. Validate platform and workflow constraints before adoption.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical workflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Fix device, build, storage, and starting state; record median, p95, worst hitch, and peak memory.&lt;/li&gt;
&lt;li&gt;Classify Read, Decode, Deserialize, and Activate in Insights.&lt;/li&gt;
&lt;li&gt;Remove unnecessary hard references and define mandatory/immediate/later/optional bundles.&lt;/li&gt;
&lt;li&gt;Add intent-driven preloading and Gameplay, Transition, and Tail budgets.&lt;/li&gt;
&lt;li&gt;Compare IoStore, Oodle, mips, audio, and PSOs under identical conditions.&lt;/li&gt;
&lt;li&gt;Add regression limits for dependency size, transition time, hitch, and memory.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;UE5 loading performance is not determined by one concurrency number.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The largest wins usually come from loading fewer dependencies and dividing assets by experience phase.&lt;/li&gt;
&lt;li&gt;Parallel I/O and a higher FPS cap should follow measurement of I/O, CPU, Game Thread, and memory behavior.&lt;/li&gt;
&lt;li&gt;IoStore, Oodle, World Partition, activation, texture residency, audio, PSOs, and GC all affect perceived loading.&lt;/li&gt;
&lt;li&gt;Custom data needs explicit Read, Decode, Apply, cancellation, and ownership boundaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Find the slow stage in Unreal Insights and why each asset is reachable in Reference Viewer. Apply parallelism, prefetching, budgets, and compression only where measurements support them. The goal is to become interactive without a large hitch afterward.&lt;/p&gt;

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

&lt;p&gt;Checked against UE 5.8 documentation on August 9, 2026. For another engine version, verify the matching documentation and Engine Source.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-5-8-release-notes" rel="noopener noreferrer"&gt;UE 5.8 Release Notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/asset-management-in-unreal-engine" rel="noopener noreferrer"&gt;Asset Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/asynchronous-asset-loading-in-unreal-engine" rel="noopener noreferrer"&gt;Asynchronous Asset Loading&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/CoreUObject/GetAsyncLoadPercentage" rel="noopener noreferrer"&gt;&lt;code&gt;GetAsyncLoadPercentage&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/zen-loader-in-unreal-engine" rel="noopener noreferrer"&gt;Zen Loader&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/oodle-data" rel="noopener noreferrer"&gt;Oodle Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/world-partition-in-unreal-engine" rel="noopener noreferrer"&gt;World Partition&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/trace-in-unreal-engine-5" rel="noopener noreferrer"&gt;Unreal Insights and Trace Channels&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Core/FFileHelper/LoadFileInBlocks" rel="noopener noreferrer"&gt;&lt;code&gt;FFileHelper::LoadFileInBlocks&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/cooking-content-and-creating-chunks-in-unreal-engine" rel="noopener noreferrer"&gt;Cooking Content and Creating Chunks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Editorial disclosure: this English edition was prepared with AI-assisted translation and editing from a technically reviewed Japanese draft. The technical claims and code excerpts were reviewed against the references above before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>unrealengine</category>
      <category>cpp</category>
      <category>gamedev</category>
      <category>performance</category>
    </item>
    <item>
      <title>Where Unity Burst and Jobs Actually Help: A Practical Guide for GameObject-Based Projects</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Sat, 22 Aug 2026 09:14:06 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/where-unity-burst-and-jobs-actually-help-a-practical-guide-for-gameobject-based-projects-4pjd</link>
      <guid>https://dev.to/gamedevtoollab/where-unity-burst-and-jobs-actually-help-a-practical-guide-for-gameobject-based-projects-4pjd</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Burst Compiler and the C# Job System can solve CPU bottlenecks, but they are not performance switches. Scheduling, copying, immediate &lt;code&gt;Complete()&lt;/code&gt; calls, and unclear NativeContainer ownership can easily erase the gain.&lt;/p&gt;

&lt;p&gt;In a GameObject-based project, performance depends on &lt;strong&gt;which calculation you isolate, which data crosses the managed boundary, and when the main thread waits&lt;/strong&gt;. This article focuses on that boundary rather than a full ECS migration or Job syntax basics.&lt;/p&gt;

&lt;p&gt;The baseline is Unity 6.3 LTS, Burst 1.8.30, Collections 2.6.8, and Mathematics 1.3.3 as of August 2026. Examples use &lt;code&gt;IJobFor&lt;/code&gt; and &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Unity.Jobs.IJobForExtensions.ScheduleParallelByRef.html" rel="noopener noreferrer"&gt;&lt;code&gt;ScheduleParallelByRef&lt;/code&gt;&lt;/a&gt;. ByRef avoids a large scheduling-time struct copy, although workers still use local copies. Older Unity versions may expose different overloads.&lt;/p&gt;

&lt;p&gt;The code demonstrates design boundaries, not a compiled drop-in sample. Validate it with Safety Checks, a Development Build, and the Profiler on target hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Burst and Jobs are a good fit
&lt;/h2&gt;

&lt;p&gt;Burst and Jobs fit workloads with many independent elements, substantial CPU work, contiguous value-type data, little Unity API access inside the kernel, and useful work available before the result is needed.&lt;/p&gt;

&lt;p&gt;Good candidates include movement, projectiles, FOV tests, LOD or culling, AI scoring, procedural geometry, noise, grids, and bulk processing. Deterministic server or replay workloads need cross-platform reproducibility tests.&lt;/p&gt;

&lt;p&gt;Poor candidates include small collections, Unity-API-heavy or immediate-result logic, managed-object-heavy processing, and cases where marshaling costs more than calculation. A compute shader may suit very large data-parallel work better.&lt;/p&gt;

&lt;p&gt;Ask: &lt;strong&gt;can you isolate a large enough calculation kernel that is closed over value-type data?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand the four separate roles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Burst Compiler&lt;/strong&gt; optimizes supported C# kernels into native code; it does not create multithreading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C# Job System&lt;/strong&gt; schedules work and tracks dependencies, with its own overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NativeContainer&lt;/strong&gt; exposes shared data under Unity's ownership and safety rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unity.Mathematics&lt;/strong&gt; provides Burst-friendly numerical types and functions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prepare data, schedule execution, and optimize the kernel as separate concerns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four questions before introducing Jobs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is CPU computation really the bottleneck?
&lt;/h3&gt;

&lt;p&gt;Profile first. Jobs do not fix frames dominated by rendering, physics, GC, asset loading, or synchronous I/O.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is each element independent?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;IJobFor.Execute(int index)&lt;/code&gt; has no guaranteed order. Shared accumulation should become per-index output followed by a reduction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can the managed boundary stay small?
&lt;/h3&gt;

&lt;p&gt;Measure input collection and result application. A faster worker kernel can still make the frame slower.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can waiting be delayed?
&lt;/h3&gt;

&lt;p&gt;Immediate &lt;code&gt;Complete()&lt;/code&gt; removes most overlap. Schedule early, consume next frame, or chain dependencies and wait only for the final handle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with &lt;code&gt;IJobFor&lt;/code&gt; and fixed-index output
&lt;/h2&gt;

&lt;p&gt;For a new parallel loop, begin with input and output that use the same index.&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.Burst&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;Unity.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;Unity.Jobs&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;Unity.Mathematics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BurstCompile&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;IntegratePositionJob&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IJobFor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ReadOnly&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;NativeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;float3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Velocities&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;NativeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;float3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Positions&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;DeltaTime&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;Execute&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="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;float3&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;Positions&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="n"&gt;Positions&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="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;Velocities&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="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;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;job&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;IntegratePositionJob&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Velocities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;velocities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Positions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;positions&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;deltaTime&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;JobHandle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ScheduleParallelByRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;positions&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;innerloopBatchCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dependency&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A NativeContainer indexer is not a &lt;code&gt;ref return&lt;/code&gt;, so “read, modify, write back” is the clearest default for structures. The outer owner allocates and disposes the arrays and must call &lt;code&gt;Complete()&lt;/code&gt; before reading or disposing data still owned by a job. &lt;code&gt;[ReadOnly]&lt;/code&gt; and fixed-index output also make the access pattern explicit to the Safety System.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical example: target evaluation with one-frame latency
&lt;/h2&gt;

&lt;p&gt;Consider a lock-on system that scores targets by distance, field of view, and threat. The main thread snapshots Transform data into persistent NativeArrays and consumes the job next frame. A version prevents stale results from reaching changed or removed targets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Input, output, and the job
&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;using&lt;/span&gt; &lt;span class="nn"&gt;Unity.Burst&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;Unity.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;Unity.Jobs&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;Unity.Mathematics&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;TargetInput&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;float3&lt;/span&gt; &lt;span class="n"&gt;Position&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;Threat&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;byte&lt;/span&gt; &lt;span class="n"&gt;IsActive&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;struct&lt;/span&gt; &lt;span class="nc"&gt;TargetEvaluation&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;DistanceSq&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;Score&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;byte&lt;/span&gt; &lt;span class="n"&gt;IsCandidate&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;BurstCompile&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;EvaluateTargetsJob&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IJobFor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ReadOnly&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;NativeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TargetInput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;WriteOnly&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;NativeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TargetEvaluation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Outputs&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;float3&lt;/span&gt; &lt;span class="n"&gt;ObserverPosition&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;float3&lt;/span&gt; &lt;span class="n"&gt;ObserverForward&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;MaxDistanceSq&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;CosHalfFieldOfView&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;Execute&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="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;TargetInput&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;Inputs&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="n"&gt;float3&lt;/span&gt; &lt;span class="n"&gt;toTarget&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;Position&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ObserverPosition&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;distanceSq&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lengthsq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toTarget&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;isNearlySamePosition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;distanceSq&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0.0001f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;float3&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalizesafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toTarget&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;facing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;isNearlySamePosition&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="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ObserverForward&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;direction&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;candidate&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;IsActive&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;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class="n"&gt;distanceSq&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;MaxDistanceSq&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class="n"&gt;facing&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;CosHalfFieldOfView&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;distanceSq01&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saturate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;distanceSq&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;math&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="n"&gt;MaxDistanceSq&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="n"&gt;Outputs&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="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TargetEvaluation&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;DistanceSq&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;distanceSq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Score&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;candidate&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;Threat&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;2.0f&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;facing&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;distanceSq01&lt;/span&gt;
                &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NegativeInfinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;IsCandidate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;candidate&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="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 job receives no managed references and writes only to matching output indices. Near-zero-distance targets pass the FOV test; tune the threshold for your scale. &lt;code&gt;float.NegativeInfinity&lt;/code&gt; is an internal sentinel, so use &lt;code&gt;Score&lt;/code&gt; only when &lt;code&gt;IsCandidate != 0&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define the target contract
&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;public&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TargetAgent&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="k"&gt;abstract&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;Threat&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;EvaluationVersion&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ApplyEvaluation&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;score&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;isCandidate&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;Read &lt;code&gt;Threat&lt;/code&gt; before scheduling, increment &lt;code&gt;EvaluationVersion&lt;/code&gt; when relevant state changes, and call &lt;code&gt;ApplyEvaluation&lt;/code&gt; only on the main thread.&lt;/p&gt;

&lt;p&gt;This example allows one frame of stale &lt;code&gt;_maxDistance&lt;/code&gt; or &lt;code&gt;_fieldOfView&lt;/code&gt;. That may suit lock-on assistance, LOD, or visual filtering, but not damage, server validation, deterministic replays, or ranking logic. Those paths need a request ID or deterministic synchronization.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is a design excerpt, not a drop-in sample. Registration APIs, capacity growth, the complete &lt;code&gt;TargetAgent&lt;/code&gt;, and tests are omitted.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&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;Unity.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;Unity.Jobs&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;Unity.Mathematics&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;TargetEvaluationSystem&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;Transform&lt;/span&gt; &lt;span class="n"&gt;_observer&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;TargetAgent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_targets&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="n"&gt;TargetAgent&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;SerializeField&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;1&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;_capacity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1024&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="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;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;_maxDistance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;30f&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="nf"&gt;Range&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;179&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;_fieldOfView&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100f&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;NativeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TargetInput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_inputs&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;NativeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TargetEvaluation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_outputs&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;TargetAgent&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_scheduledTargets&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="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_scheduledVersions&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;JobHandle&lt;/span&gt; &lt;span class="n"&gt;_pending&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;_pendingCount&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;_isDisposed&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="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&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;_capacity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_inputs&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;NativeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TargetInput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Allocator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;NativeArrayOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UninitializedMemory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_outputs&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;NativeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TargetEvaluation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Allocator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;NativeArrayOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UninitializedMemory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_scheduledTargets&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;TargetAgent&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;_scheduledVersions&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;int&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;capacity&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;CompleteAndApply&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;_isDisposed&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isActiveAndEnabled&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;_observer&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;return&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;targetCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_targets&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="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;targetCount&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;_inputs&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="s"&gt;"Target count exceeds NativeArray capacity."&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;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&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="n"&gt;targetCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_inputs&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="k"&gt;for&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;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="n"&gt;count&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="n"&gt;TargetAgent&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;_targets&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="n"&gt;_scheduledTargets&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="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;_scheduledVersions&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="n"&gt;target&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;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EvaluationVersion&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;if&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="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;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isActiveAndEnabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;_inputs&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;default&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;Vector3&lt;/span&gt; &lt;span class="n"&gt;p&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;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;_inputs&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;new&lt;/span&gt; &lt;span class="n"&gt;TargetInput&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="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;float3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&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;p&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;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;Threat&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;Threat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;IsActive&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;count&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;return&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;op&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_observer&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;Vector3&lt;/span&gt; &lt;span class="n"&gt;forward&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;forward&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;maxDistanceSq&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_maxDistance&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;_maxDistance&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;job&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;EvaluateTargetsJob&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Inputs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Outputs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_outputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ObserverPosition&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;float3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&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;op&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;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;ObserverForward&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalizesafe&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;float3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forward&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;forward&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;forward&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
            &lt;span class="n"&gt;MaxDistanceSq&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxDistanceSq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;CosHalfFieldOfView&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_fieldOfView&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="p"&gt;};&lt;/span&gt;

        &lt;span class="n"&gt;_pending&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ScheduleParallelByRef&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="m"&gt;64&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;_pendingCount&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="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;CompleteAndApply&lt;/span&gt;&lt;span class="p"&gt;()&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;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_pendingCount&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;count&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;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;_pending&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;_pendingCount&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;try&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;int&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="n"&gt;count&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="n"&gt;TargetAgent&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;_scheduledTargets&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="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="n"&gt;_scheduledVersions&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="k"&gt;if&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="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;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isActiveAndEnabled&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;EvaluationVersion&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="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;TargetEvaluation&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;_outputs&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="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ApplyEvaluation&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;Score&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;IsCandidate&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_isDisposed&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isActiveAndEnabled&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="nf"&gt;ClearScheduledReferences&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="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;OnDisable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CompleteWithoutApply&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;_isDisposed&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;CompleteWithoutApply&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;_inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCreated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;_inputs&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCreated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;_outputs&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="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CompleteWithoutApply&lt;/span&gt;&lt;span class="p"&gt;()&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;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_pendingCount&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;count&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;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;_pending&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;_pendingCount&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="nf"&gt;ClearScheduledReferences&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="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;ClearScheduledReferences&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;count&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;Array&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="n"&gt;_scheduledTargets&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;count&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;Array&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="n"&gt;_scheduledVersions&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;count&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 next &lt;code&gt;Update&lt;/code&gt; completes the previous job before reusing the arrays. Removal code must also advance the target version.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;finally&lt;/code&gt; clears managed references but does not swallow exceptions. This sample is fail-fast; a system that must continue should catch individual callbacks and define recovery.&lt;/p&gt;

&lt;p&gt;Capacity overflow triggers &lt;code&gt;Debug.Assert&lt;/code&gt;; production code should define a hard limit or resize only after the pending job completes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build pipelines instead of isolated jobs
&lt;/h2&gt;

&lt;p&gt;Pass one job's handle into the next instead of completing between stages. Use &lt;code&gt;JobHandle.CombineDependencies&lt;/code&gt; when several independent jobs feed one later stage. Keep data in NativeContainers through evaluation and reduction, then copy only the smallest required result back to GameObjects.&lt;/p&gt;

&lt;p&gt;Do not let parallel iterations update one shared maximum. Write one score per index, then run a dependent single &lt;code&gt;IJob&lt;/code&gt; reduction. A linear final pass is often cheap and gives deterministic tie-breaking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Variable-length output and &lt;code&gt;ParallelWriter&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Use a container-specific writer when parallel iterations append results.&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;ReadOnly&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;NativeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TargetEvaluation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Evaluations&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;NativeList&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;ParallelWriter&lt;/span&gt; &lt;span class="n"&gt;CandidateIndices&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;Execute&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="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;Evaluations&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="n"&gt;IsCandidate&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;CandidateIndices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNoResize&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AddNoResize&lt;/code&gt; requires enough Capacity before scheduling. When reusing the list, complete the previous writer, call &lt;code&gt;Clear()&lt;/code&gt; on the main thread, verify Capacity, and schedule again.&lt;/p&gt;

&lt;p&gt;Parallel append order is undefined. If order affects state, sort afterward, compact fixed flags in source order, or preserve source indices with an explicit tie rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  NativeContainer lifetime and batch size
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;Temp&lt;/code&gt; for extremely short-lived data, &lt;code&gt;TempJob&lt;/code&gt; for short jobs disposed within four frames, and &lt;code&gt;Persistent&lt;/code&gt; for explicitly owned buffers. For every-frame work, allocate persistent capacity once and reuse it. One owner should control allocation, the outstanding handle, resizing, and disposal. Never resize or dispose while a job may access the memory; call &lt;code&gt;Complete()&lt;/code&gt; even when &lt;code&gt;IsCompleted&lt;/code&gt; is true. See the &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/job-system-native-container.html" rel="noopener noreferrer"&gt;NativeContainer manual&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Batch size also needs measurement. Start around 64 for light work, 16 or 32 for medium work, and 1 for expensive iterations; compare average time, worst frames, and the wait at &lt;code&gt;Complete()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  GameObjects, Transforms, and data boundaries
&lt;/h2&gt;

&lt;p&gt;A Burst job cannot directly access a normal &lt;code&gt;GameObject&lt;/code&gt; or &lt;code&gt;MonoBehaviour&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot values:&lt;/strong&gt; copy Transform data into NativeArrays. This suits multi-stage numerical pipelines, but collection and application costs must be measured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;IJobParallelForTransform&lt;/code&gt;:&lt;/strong&gt; process a &lt;code&gt;TransformAccessArray&lt;/code&gt;. Use read-write scheduling when modifying Transforms and read-only scheduling when reading; read-only jobs may receive invalid entries, so check &lt;code&gt;TransformAccess.isValid&lt;/code&gt;. Frequent additions and removals make maintenance and index mapping part of the cost. See &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Jobs.IJobParallelForTransformExtensions.html" rel="noopener noreferrer"&gt;&lt;code&gt;IJobParallelForTransformExtensions&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write kernels Burst can optimize
&lt;/h2&gt;

&lt;p&gt;Keep managed references outside the kernel and copy only required values before scheduling. Prefer Unity.Mathematics and squared-distance comparisons for numerical work.&lt;/p&gt;

&lt;p&gt;Do not enable &lt;code&gt;FloatMode.Fast&lt;/code&gt; automatically. Approximate visuals may tolerate it; replay, server validation, and cross-device agreement require error and reproducibility tests.&lt;/p&gt;

&lt;p&gt;Per-element &lt;code&gt;FunctionPointer&amp;lt;T&amp;gt;&lt;/code&gt; calls retain overhead and can block wider vectorization. Prefer direct job code or batched calls; see Burst's &lt;a href="https://docs.unity3d.com/Packages/com.unity.burst@1.8/manual/csharp-function-pointers.html" rel="noopener noreferrer"&gt;Function Pointer guidance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Treat Safety System errors as design feedback. Prefer fixed-index output, dependency chains, staged reductions, or supported writers before disabling restrictions. Pass mutable settings through the job struct, not mutable static state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invalidate results instead of canceling jobs
&lt;/h2&gt;

&lt;p&gt;A scheduled job cannot be interrupted halfway through. When a result becomes obsolete, let the job finish and reject it with a generation or request ID. Split very long work into shorter jobs. This is usually safer than a cancellation flag because the main thread cannot freely mutate shared job memory after scheduling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the complete system
&lt;/h2&gt;

&lt;p&gt;Compare a normal C# loop, &lt;code&gt;RunByRef&lt;/code&gt;, &lt;code&gt;ScheduleByRef&lt;/code&gt;, and &lt;code&gt;ScheduleParallelByRef&lt;/code&gt;. Measure collection, scheduling, &lt;code&gt;Complete()&lt;/code&gt; wait, result application, and the whole frame. Reject the design if the kernel improves but the full boundary becomes slower.&lt;/p&gt;

&lt;p&gt;Profile after Burst warm-up and decide in a Development Build on target hardware. For &lt;code&gt;WaitForJobGroup&lt;/code&gt;, inspect Timeline: completion timing, scheduling timing, dependency serialization, worker occupancy, and Burst compilation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical adoption sequence
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Extract numerical work from &lt;code&gt;MonoBehaviour.Update&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Express inputs and outputs as value-type arrays&lt;/li&gt;
&lt;li&gt;Validate results, disposal, invalidation, and scene transitions with &lt;code&gt;RunByRef&lt;/code&gt; or &lt;code&gt;ScheduleByRef&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Switch to &lt;code&gt;ScheduleParallelByRef&lt;/code&gt; and measure batch size and independence&lt;/li&gt;
&lt;li&gt;Schedule earlier and delay &lt;code&gt;Complete()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Split work into dependent jobs only when profiling supports it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Common failures are per-frame allocation, immediate completion, many tiny jobs, excessive copying, suppressed safety errors, and assumed output order. Fix data and synchronization before adding more jobs.&lt;/p&gt;

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

&lt;p&gt;With Burst and the Job System, data boundaries and synchronization matter more than syntax.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep Unity API access outside the kernel&lt;/li&gt;
&lt;li&gt;Reuse NativeContainers and complete jobs before reading, resizing, or disposal&lt;/li&gt;
&lt;li&gt;Chain work instead of waiting immediately&lt;/li&gt;
&lt;li&gt;Validate stale results and variable output order explicitly&lt;/li&gt;
&lt;li&gt;Measure the entire path on target hardware&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with many similar elements and a result that can tolerate one frame of latency. Expand only where profiling proves a benefit.&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/job-system-native-container.html" rel="noopener noreferrer"&gt;Unity Manual: NativeContainer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Unity.Jobs.IJobForExtensions.html" rel="noopener noreferrer"&gt;Unity Scripting API: IJobForExtensions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Jobs.IJobParallelForTransformExtensions.html" rel="noopener noreferrer"&gt;Unity Scripting API: IJobParallelForTransformExtensions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Unity.Jobs.JobHandle.Complete.html" rel="noopener noreferrer"&gt;Unity Scripting API: JobHandle.Complete&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This article was created with AI-assisted drafting. The author reviewed the technical content and makes the final publication decision.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>gamedev</category>
      <category>performance</category>
    </item>
    <item>
      <title>Unity Mipmaps Beyond Smaller Textures: Temporal Stability, Streaming, and Semantic Mips</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Wed, 19 Aug 2026 07:31:55 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/unity-mipmaps-beyond-smaller-textures-temporal-stability-streaming-and-semantic-mips-38ck</link>
      <guid>https://dev.to/gamedevtoollab/unity-mipmaps-beyond-smaller-textures-temporal-stability-streaming-and-semantic-mips-38ck</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Mipmaps are usually introduced as smaller copies of a texture. That is correct, but it does not explain why a no-mipmap screenshot looks sharp while motion shimmers, why foliage disappears, why normal-mapped highlights flash, or why an atlas bleeds only in lower levels.&lt;/p&gt;

&lt;p&gt;A more useful model treats a mip chain as three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A tool for &lt;strong&gt;temporal image stability&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A mechanism for &lt;strong&gt;allocating bandwidth and resident memory&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A hierarchy whose levels can preserve different kinds of &lt;strong&gt;meaning&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This article uses Unity 6.5 (6000.5) terminology. Inspector names can differ in Unity 2022/2023 LTS, other Unity 6 versions, and between URP and HDRP package versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mipmaps Match Sampling Density, Not Distance
&lt;/h2&gt;

&lt;p&gt;Imagine a 4096 x 4096 texture on a floor viewed at a shallow angle. One screen pixel may cover hundreds of texels. Sampling only mip 0 selects a tiny subset of that footprint, so a small camera movement selects a different subset and produces crawling detail, moire, and flicker.&lt;/p&gt;

&lt;p&gt;A mipmap selects a texel density closer to the projected footprint. Distance matters, but it is not the rule. Projected size, UV scale, surface angle, texture resolution, projection, bias, and anisotropy all affect the result.&lt;/p&gt;

&lt;p&gt;For ordinary implicit sampling, the GPU estimates UV change across neighboring fragments. In simplified form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rho = max(length(ddx(texelPosition)), length(ddy(texelPosition)))
lod = log2(rho)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An LOD near 0 points to mip 0; near 1 points to mip 1. Hardware is more sophisticated, especially with anisotropy, but the key model is: &lt;strong&gt;screen-space UV derivatives drive mip selection&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bilinear, trilinear, and anisotropic filtering solve different problems
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bilinear:&lt;/strong&gt; blends texels inside one mip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trilinear:&lt;/strong&gt; blends two adjacent mips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anisotropic:&lt;/strong&gt; handles elongated footprints on oblique surfaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trilinear filtering does not replace anisotropic filtering. A road may need anisotropic sampling even when mip transitions are smooth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ideal LOD and resident quality are separate
&lt;/h3&gt;

&lt;p&gt;The sampler may want mip 0 while Streaming currently has only mip 2 and coarser levels resident. This explains why a surface can be blurry after a camera cut and sharpen later. Always separate the ideal sample LOD from the highest-resolution mip actually available in GPU memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 33% Cost Hides a More Useful Fact
&lt;/h2&gt;

&lt;p&gt;For a square texture, the complete chain is approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mip 0: 4096 x 4096
Mip 1: 2048 x 2048
Mip 2: 1024 x 1024
...
Mip 12: 1 x 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each level contains one quarter as many texels as the previous one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 + 1/4 + 1/16 + 1/64 + ... = 4/3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a full mip chain adds about 33% more texels than mip 0 alone. The reverse view is more useful for budgeting:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Range&lt;/th&gt;
&lt;th&gt;Approximate share of the full chain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mip 0&lt;/td&gt;
&lt;td&gt;75.00%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mip 1&lt;/td&gt;
&lt;td&gt;18.75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mip 2&lt;/td&gt;
&lt;td&gt;4.69%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mip 3&lt;/td&gt;
&lt;td&gt;1.17%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mip 4 and below combined&lt;/td&gt;
&lt;td&gt;0.39%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mip 0 alone is roughly three quarters of the chain. Dropping the maximum resolution by one level does not halve the texel count; it reduces the remaining chain to about one quarter. Two levels reduce it to about one sixteenth.&lt;/p&gt;

&lt;p&gt;Compression blocks, minimum mip storage, alignment, and platform details change exact byte counts, but this approximation explains why Mipmap Limits and Mipmap Streaming can have such large effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why “No Mipmaps” Can Win a Screenshot Comparison
&lt;/h2&gt;

&lt;p&gt;Disabling mipmaps can sharpen one frame while making motion unstable. Test fine grids, gravel, leaves, thin text, and repeating patterns with a slow, repeatable camera pan. Temporarily disabling TAA can help isolate texture aliasing.&lt;/p&gt;

&lt;p&gt;Watch for crawling patterns, distant moire, flashing highlights, oblique-surface noise, and discontinuities between render scales. Mipmaps are spatial prefilters, but in practice they are also &lt;strong&gt;temporal noise reduction&lt;/strong&gt;. Judge negative mip bias the same way: in motion, not from one sharp frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure Import Settings by Meaning, Not File Extension
&lt;/h2&gt;

&lt;p&gt;Two PNG files can require opposite settings. Decide from what the shader expects the texels to mean.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Texture meaning&lt;/th&gt;
&lt;th&gt;sRGB&lt;/th&gt;
&lt;th&gt;Mipmaps&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;Base color / ordinary emissive color&lt;/td&gt;
&lt;td&gt;Usually on&lt;/td&gt;
&lt;td&gt;Usually on in 3D&lt;/td&gt;
&lt;td&gt;Alpha coverage; HDR/data exceptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Normal map&lt;/td&gt;
&lt;td&gt;Import as Normal Map&lt;/td&gt;
&lt;td&gt;Usually on&lt;/td&gt;
&lt;td&gt;Average direction loses normal variance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roughness / metallic / AO&lt;/td&gt;
&lt;td&gt;Off&lt;/td&gt;
&lt;td&gt;Often on&lt;/td&gt;
&lt;td&gt;Numerical meaning after filtering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Material ID / category&lt;/td&gt;
&lt;td&gt;Off&lt;/td&gt;
&lt;td&gt;Use cautiously&lt;/td&gt;
&lt;td&gt;Averaging can invent invalid IDs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed-size UI&lt;/td&gt;
&lt;td&gt;Usually on&lt;/td&gt;
&lt;td&gt;Often unnecessary&lt;/td&gt;
&lt;td&gt;Reconsider for scaling, rotation, world space&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel art&lt;/td&gt;
&lt;td&gt;Pipeline-dependent&lt;/td&gt;
&lt;td&gt;Usually off or custom&lt;/td&gt;
&lt;td&gt;Point sampling and integer scaling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lookup/data texture&lt;/td&gt;
&lt;td&gt;Off&lt;/td&gt;
&lt;td&gt;Usually off&lt;/td&gt;
&lt;td&gt;Automatic averaging may corrupt data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  sRGB is semantic
&lt;/h3&gt;

&lt;p&gt;Base color is normally color. Roughness, metallic, AO, height, vectors, coefficients, LUT values, and masks are normally linear data. HDR textures and emissive maps that store numerical intensity are common exceptions. Because mip generation is filtering, the wrong color space changes the meaning of lower levels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Box, Kaiser, and negative bias
&lt;/h3&gt;

&lt;p&gt;Unity's Box filter is smoother; Kaiser retains more sharpness and potentially more shimmer. Negative bias also selects higher-resolution levels, increasing aliasing, bandwidth, cache pressure, and streaming demand. Before using it globally, check anisotropic filtering, UV density, source resolution, render scale, and the active upscaler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Case 1: Foliage and Fences Disappear
&lt;/h2&gt;

&lt;p&gt;An alpha-tested shader may contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hlsl"&gt;&lt;code&gt;&lt;span class="nb"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alpha&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="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A thin leaf may be alpha 1 against alpha 0. Downsampling creates boundary values such as 0.4, which fail a 0.5 cutoff. The average is not mathematically wrong; the &lt;strong&gt;area that survives the threshold&lt;/strong&gt;, or coverage, changed between mip levels.&lt;/p&gt;

&lt;p&gt;Unity's &lt;strong&gt;Preserve Coverage&lt;/strong&gt; adjusts lower levels to keep coverage more stable. Match its cutoff to the shader and check the base pass, shadow-caster pass, Shader Graph threshold, LOD materials, and any Alpha To Coverage path. If the object remains visible but its shadow thins, inspect the shadow pass first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replicate Border&lt;/strong&gt; is different: it preserves image-edge values for cases such as light cookies. It does not replace Preserve Coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Case 2: Normal Maps Still Produce Flashing Highlights
&lt;/h2&gt;

&lt;p&gt;Mipmapping averages normal directions, but an average normal does not describe their distribution. Equal left- and right-facing normals may average forward; that does not make the region smooth. It means variance was discarded. Keeping the original smoothness can then create a sharp, unstable specular lobe around the average direction.&lt;/p&gt;

&lt;p&gt;Treat normal filtering and specular stability separately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;import the normal map correctly and give it mipmaps;&lt;/li&gt;
&lt;li&gt;keep roughness/smoothness linear;&lt;/li&gt;
&lt;li&gt;feed lost normal variance into roughness when possible;&lt;/li&gt;
&lt;li&gt;consider HDRP's Geometric Specular Anti-Aliasing;&lt;/li&gt;
&lt;li&gt;in custom shaders, reduce smoothness from normal variation or LOD.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MSAA mainly addresses polygon edges. TAA can hide some instability, but unstable input often trades shimmer for blur or ghosting. Stabilize the material before temporal reconstruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Case 3: Packed Masks Share One Mip Policy
&lt;/h2&gt;

&lt;p&gt;Packing metallic, AO, roughness, and other masks into RGBA reduces samples and memory, but all channels still share mip generation, filtering, wrap mode, sRGB state, compression, Mipmap Limit Group, streaming priority, and bias.&lt;/p&gt;

&lt;p&gt;Mip filtering operates on channel values, while compression error is not guaranteed to be independent per channel. More importantly, sampling and residency policy are selected at texture granularity. Pack values because they can share that policy, not merely because a channel is empty.&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;Desired reduction behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AO&lt;/td&gt;
&lt;td&gt;Smooth aggregation may be acceptable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roughness&lt;/td&gt;
&lt;td&gt;Specular energy matters, not only arithmetic mean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binary mask&lt;/td&gt;
&lt;td&gt;Preserve area, majority, or max depending on meaning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Material ID&lt;/td&gt;
&lt;td&gt;Never invent an intermediate category&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signed direction&lt;/td&gt;
&lt;td&gt;May require renormalization or reinterpretation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For IDs and categories, consider a separate non-mipmapped texture, point sampling, custom majority/max mips, or a buffer/spatial structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Case 4: Atlas Bleeding Gets Worse at Lower Mips
&lt;/h2&gt;

&lt;p&gt;Four pixels of mip-0 padding become two in mip 1, one in mip 2, and half a pixel in mip 3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;effective padding at mip L ~= mip 0 padding / 2^L
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Design padding for the lowest level you expect to use. Dilate island colors, increase padding, keep UVs inside their islands, and distinguish texture-edge problems from neighbors inside the atlas.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;Texture2DArray&lt;/code&gt; isolates layers and avoids much atlas bleeding, but Unity's Mipmap Streaming does not support array textures. Balance visual stability, batching, streaming, and platform support.&lt;/p&gt;

&lt;p&gt;Hand-authored lower mips can go further: detailed sign text can become a bold pictogram at distance. That is the basis of semantic mipmaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mipmap Limits and Streaming as a Quality Scheduler
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Mipmap Limit&lt;/strong&gt; caps allowed quality; &lt;strong&gt;Mipmap Streaming&lt;/strong&gt; decides which allowed levels are resident. Group textures by visual importance, such as &lt;code&gt;CharacterHero&lt;/code&gt;, &lt;code&gt;CharacterCrowd&lt;/code&gt;, &lt;code&gt;WorldNear&lt;/code&gt;, &lt;code&gt;WorldFar&lt;/code&gt;, and &lt;code&gt;Cinematic&lt;/code&gt;. One removed top level has a large effect because mip 0 was about 75% of the chain.&lt;/p&gt;

&lt;p&gt;Priority is relative, not a reservation for mip 0. A budget shortage can prevent a requested level from loading, so never wait forever. This is a small cutscene-oriented example; a general preloader also needs ownership, cancellation, limit handling, camera-change handling, destruction safety, and budget monitoring.&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;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;MipmapPreloader&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;Texture2D&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;textures&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;SerializeField&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;0&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;requestedLevel&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="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.1f&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;timeoutSeconds&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;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;Preload&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="n"&gt;Texture2D&lt;/span&gt; &lt;span class="n"&gt;texture&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;textures&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;CanStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requestedMipmapLevel&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;Clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;requestedLevel&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;texture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mipmapCount&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="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;deadline&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;realtimeSinceStartup&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;while&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;realtimeSinceStartup&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&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;complete&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;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Texture2D&lt;/span&gt; &lt;span class="n"&gt;texture&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;textures&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;CanStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="n"&gt;complete&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsRequestedMipmapLevelLoaded&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;complete&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="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;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;"Mip preload timed out; continuing with fallback quality."&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;Release&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="n"&gt;Texture2D&lt;/span&gt; &lt;span class="n"&gt;texture&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;textures&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;CanStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ClearRequestedMipmapLevel&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;OnDisable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Release&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;bool&lt;/span&gt; &lt;span class="nf"&gt;CanStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Texture2D&lt;/span&gt; &lt;span class="n"&gt;texture&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;texture&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;texture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;streamingMipmaps&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;Define the timeout fallback: continue at lower quality, extend a fade, or reduce the request. Release control after the cutscene. Centralize ownership because one caller's &lt;code&gt;ClearRequestedMipmapLevel&lt;/code&gt; can clear another caller's expectation.&lt;/p&gt;

&lt;p&gt;Unity estimates required levels from meshes, UVs, cameras, and standard material conventions. Array textures, cubemap arrays, and 3D textures are unsupported by Mipmap Streaming; custom drawing and nonstandard UV transforms also need verification.&lt;/p&gt;

&lt;p&gt;Success is not “Streaming is enabled.” It is that &lt;strong&gt;required-mip estimation and the memory budget remain valid during real camera motion on target hardware&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visualize the Approximate Mip Level
&lt;/h2&gt;

&lt;p&gt;Subjective reports such as “slightly blurry on one device” are easier to investigate when approximate LOD is visible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hlsl"&gt;&lt;code&gt;&lt;span class="n"&gt;float&lt;/span&gt; &lt;span class="nf"&gt;ApproximateMipLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float2&lt;/span&gt; &lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;float4&lt;/span&gt; &lt;span class="n"&gt;texelSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// texelSize = (1/width, 1/height, width, height)&lt;/span&gt;
    &lt;span class="kt"&gt;float2&lt;/span&gt; &lt;span class="n"&gt;texelPosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uv&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;texelSize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;float2&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;ddx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texelPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;float2&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;ddy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texelPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;float&lt;/span&gt; &lt;span class="n"&gt;footprint2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dy&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;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="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;footprint2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e-8&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="mi"&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;Color &lt;code&gt;floor(lod)&lt;/code&gt; bands to reveal UV-density discontinuities, material bias differences, oblique surfaces, dynamic-resolution changes, and the mip where an atlas starts bleeding.&lt;/p&gt;

&lt;p&gt;This is diagnostic, not an exact sampler result. Real sampling also accounts for anisotropy, bias, and platform details. Calculate derivatives before divergent per-fragment branches whenever possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 1: Semantic Mipmaps
&lt;/h2&gt;

&lt;p&gt;A mip level can carry a representation designed for its scale rather than an automatic reduction. I will call this a &lt;strong&gt;semantic mipmap&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;replace a shop name with its icon at distance;&lt;/li&gt;
&lt;li&gt;remove minor roads from lower map levels;&lt;/li&gt;
&lt;li&gt;thicken selection or emissive marks in coarse mips;&lt;/li&gt;
&lt;li&gt;fade a detail normal toward neutral;&lt;/li&gt;
&lt;li&gt;reduce a binary mask by majority or max instead of average.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is texture LOD: the representation changes, not only its resolution.&lt;/p&gt;

&lt;p&gt;Trilinear filtering blends adjacent levels. If mip 2 is text and mip 3 is an icon, the transition contains both. Design blendable levels, transition over several mips, use explicit LOD, or blend separate textures.&lt;/p&gt;

&lt;p&gt;Filter and wrap modes are semantic too. A sign may use &lt;code&gt;Trilinear&lt;/code&gt; and &lt;code&gt;Clamp&lt;/code&gt;; an ID hierarchy may require &lt;code&gt;Point&lt;/code&gt; or explicit LOD. Point filtering stops interpolation but cannot repair a mip generated with the wrong reduction rule.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Texture2D.SetPixelData&lt;/code&gt; can write every level. The critical detail is &lt;code&gt;Apply(updateMipmaps: false)&lt;/code&gt;; otherwise Unity regenerates the chain from mip 0.&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="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;SemanticMipTextureBuilder&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;Texture2D&lt;/span&gt; &lt;span class="nf"&gt;Create&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;width&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;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Color32&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;mips&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;linear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;FilterMode&lt;/span&gt; &lt;span class="n"&gt;filterMode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;TextureWrapMode&lt;/span&gt; &lt;span class="n"&gt;wrapMode&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;width&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentOutOfRangeException&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;width&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;height&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentOutOfRangeException&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;height&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="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FloorToInt&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;Log&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="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&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="m"&gt;1&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;mips&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;mips&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="n"&gt;expected&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;$"Exactly &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="s"&gt; mip levels are required."&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;mipWidth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;width&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;mipHeight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;height&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;int&lt;/span&gt; &lt;span class="n"&gt;level&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;level&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&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;level&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;mips&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;level&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="n"&gt;mips&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;level&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;mipWidth&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;mipHeight&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;$"Mip &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; has an invalid pixel count."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;mipWidth&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;mipWidth&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&amp;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;mipHeight&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;mipHeight&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&amp;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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;texture&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;Texture2D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TextureFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RGBA32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;mipChain&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;linear&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;filterMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filterMode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;wrapMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wrapMode&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;int&lt;/span&gt; &lt;span class="n"&gt;level&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;level&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&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;level&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
            &lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetPixelData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mips&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updateMipmaps&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="n"&gt;makeNoLongerReadable&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="n"&gt;texture&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 shipped assets, an editor or external pipeline that generates levels before platform compression is often easier to manage. Use &lt;code&gt;makeNoLongerReadable: true&lt;/code&gt; when the CPU no longer needs the texture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 2: Use the Mip Chain as a Hierarchical Data Structure
&lt;/h2&gt;

&lt;p&gt;A mip chain repeatedly aggregates each 2 x 2 region. The reduction need not be an average:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Average:&lt;/strong&gt; luminance, exposure, bloom;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Min / Max:&lt;/strong&gt; regional extrema;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Depth pyramid:&lt;/strong&gt; occlusion and screen-space effects;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Occupancy:&lt;/strong&gt; reject empty coarse regions before reading detail;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variance:&lt;/strong&gt; preserve mean and dispersion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For custom reduction, create a mipmapped &lt;code&gt;RenderTexture&lt;/code&gt;, disable &lt;code&gt;autoGenerateMips&lt;/code&gt;, and dispatch a compute shader from level 0 to 1, then 1 to 2. &lt;code&gt;GenerateMips&lt;/code&gt; performs ordinary generation; it does not express max, majority vote, or arbitrary aggregation.&lt;/p&gt;

&lt;p&gt;This is only an outline. Enable &lt;code&gt;enableRandomWrite&lt;/code&gt; before creation, verify &lt;code&gt;GraphicsFormat&lt;/code&gt; random-write support, bind the destination mip with &lt;code&gt;ComputeShader.SetTexture(..., mipLevel)&lt;/code&gt;, define ordering and synchronization, and test every target graphics API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mip 0: source 16 x 16
  -&amp;gt; max over each 2 x 2 block
Mip 1: 8 x 8 -&amp;gt; Mip 2: 4 x 4 -&amp;gt; Mip 3: 2 x 2 -&amp;gt; Mip 4: 1 x 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A coarse occupancy level can terminate a search before fine reads. For depth pyramids, min/max meaning reverses between conventional Z and reversed Z.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 3: Automate Import Rules by Semantic Suffix
&lt;/h2&gt;

&lt;p&gt;Many mip failures are configuration drift. Avoid an ambiguous &lt;code&gt;_mask&lt;/code&gt; suffix and split conventions by reduction behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;_orm&lt;/code&gt;: continuous linear data with mipmaps;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_cutout&lt;/code&gt;: alpha-tested color with Preserve Coverage;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_coverage&lt;/code&gt;: linear coverage data with Preserve Coverage;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_binary&lt;/code&gt; / &lt;code&gt;_id&lt;/code&gt;: discrete values, no automatic mipmaps, point filtering;&lt;/li&gt;
&lt;li&gt;separate suffixes for majority, max, or authored semantic mips.
&lt;/li&gt;
&lt;/ul&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="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;TextureImportConvention&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AssetPostprocessor&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;uint&lt;/span&gt; &lt;span class="nf"&gt;GetVersion&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;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;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnPreprocessTexture&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;importer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TextureImporter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;assetImporter&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;assetPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'\\'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sc"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToLowerInvariant&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/textures/world/"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mipmapEnabled&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;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filterMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FilterMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Trilinear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;streamingMipmaps&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;HasSuffix&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;"_orm"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sRGBTexture&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="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mipmapEnabled&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;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filterMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FilterMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Trilinear&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;HasSuffix&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;"_cutout"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;HasSuffix&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;"_coverage"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mipmapEnabled&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;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filterMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FilterMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Trilinear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mipMapsPreserveCoverage&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;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alphaTestReferenceValue&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;HasSuffix&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;"_coverage"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sRGBTexture&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;HasSuffix&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;"_binary"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;HasSuffix&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;"_id"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sRGBTexture&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="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mipmapEnabled&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="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;streamingMipmaps&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="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filterMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FilterMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Point&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;bool&lt;/span&gt; &lt;span class="nf"&gt;HasSuffix&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;suffix&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndsWith&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;suffix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.png"&lt;/span&gt;&lt;span class="p"&gt;)&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;EndsWith&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;suffix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.tga"&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;Place this in an &lt;code&gt;Editor&lt;/code&gt; folder or editor-only assembly. Keep the cutoff synchronized with the shader. Separate mandatory rules, first-import defaults, and convention warnings. Increment &lt;code&gt;GetVersion()&lt;/code&gt; when behavior changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamic Resolution, Upscalers, and Mip Bias
&lt;/h2&gt;

&lt;p&gt;Lower internal resolution changes UV derivatives and can select coarser mips. A strong global negative bias may sharpen a still image while increasing shimmer, bandwidth, and streaming pressure.&lt;/p&gt;

&lt;p&gt;Check the active pipeline and upscaler first. HDRP provides &lt;strong&gt;Use Mip Bias&lt;/strong&gt; for Dynamic Resolution, and its DLSS integration can apply automatic correction. URP/HDRP, DLSS/FSR/STP, package versions, and custom shaders do not apply bias in the same place, so avoid correcting it twice at pipeline, texture, and shader levels.&lt;/p&gt;

&lt;p&gt;Compare stationary detail, slow-pan shimmer, GPU time, streaming-budget pressure, and render-scale transitions on one camera path. Output sharpening and sampler bias are different: one modifies the reconstructed image; the other introduces higher-frequency texture input.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Decision Flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Will it shrink or rotate on screen?&lt;/strong&gt; Use mipmaps for most 3D textures; reconsider UI that scales, rotates, or enters world space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Color or data?&lt;/strong&gt; Use sRGB for ordinary color, linear for numerical data, and the Normal Map importer for normals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alpha tested?&lt;/strong&gt; Match Preserve Coverage with every shader pass that clips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Viewed obliquely?&lt;/strong&gt; Try anisotropic filtering before negative bias.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atlased?&lt;/strong&gt; Derive padding and dilation from the lowest required mip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory constrained?&lt;/strong&gt; Prefer Mipmap Limit Groups and Streaming over disabling mipmaps globally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does averaging preserve meaning?&lt;/strong&gt; Use ordinary generation for color-like signals, Preserve Coverage for cutouts, custom min/max/majority reduction for special data, and semantic mips when the representation should change.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common Mipmap Myths
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Myth&lt;/th&gt;
&lt;th&gt;Better model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Distance alone selects the mip&lt;/td&gt;
&lt;td&gt;Screen-space UV derivatives select the ideal LOD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No mipmaps means higher quality&lt;/td&gt;
&lt;td&gt;It may sharpen a still frame while increasing temporal aliasing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trilinear fixes an oblique floor&lt;/td&gt;
&lt;td&gt;Anisotropic footprints need anisotropic filtering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A mip chain nearly doubles memory&lt;/td&gt;
&lt;td&gt;The theoretical increase is about 33%; mip 0 is about 75% of the chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High priority guarantees mip 0&lt;/td&gt;
&lt;td&gt;Requests can lose to the memory budget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Normal mipmaps guarantee stable highlights&lt;/td&gt;
&lt;td&gt;Average direction does not preserve normal variance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any empty packed channel is free&lt;/td&gt;
&lt;td&gt;All channels share filtering, compression, and residency policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atlas padding is a mip-0 decision&lt;/td&gt;
&lt;td&gt;Effective padding halves at every level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mips must be automatic reductions&lt;/td&gt;
&lt;td&gt;Levels can contain semantic replacements or custom aggregates&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;Mipmaps connect image stability, bandwidth, resident memory, material semantics, and scalable representation. The central question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When this texture becomes smaller on screen, what information must survive?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Color may need an average; foliage needs coverage; normals need variance; IDs need category integrity; signs need readability; hierarchical depth needs min or max. Treat every level as a deliberate representation, and mipmaps become a design tool rather than an import checkbox.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/texture-mipmaps-introduction.html" rel="noopener noreferrer"&gt;Unity 6.5: Mipmaps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/texture-type-default.html" rel="noopener noreferrer"&gt;Unity 6.5: Default Texture Import Settings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/class-QualitySettings.html" rel="noopener noreferrer"&gt;Unity 6.5: Quality Settings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/TextureStreaming-introduction.html" rel="noopener noreferrer"&gt;Unity 6.5: Mipmap Streaming&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Texture-mipMapBias.html" rel="noopener noreferrer"&gt;Unity API: Texture.mipMapBias&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Texture2D-requestedMipmapLevel.html" rel="noopener noreferrer"&gt;Unity API: Texture2D.requestedMipmapLevel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Texture2D.SetPixelData.html" rel="noopener noreferrer"&gt;Unity API: Texture2D.SetPixelData&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Texture2D.Apply.html" rel="noopener noreferrer"&gt;Unity API: Texture2D.Apply&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Texture2D.IsRequestedMipmapLevelLoaded.html" rel="noopener noreferrer"&gt;Unity API: Texture2D.IsRequestedMipmapLevelLoaded&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Rendering.CommandBuffer.GenerateMips.html" rel="noopener noreferrer"&gt;Unity API: CommandBuffer.GenerateMips&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/RenderTexture-enableRandomWrite.html" rel="noopener noreferrer"&gt;Unity API: RenderTexture.enableRandomWrite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/ComputeShader.SetTexture.html" rel="noopener noreferrer"&gt;Unity API: ComputeShader.SetTexture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/FilterMode.html" rel="noopener noreferrer"&gt;Unity API: FilterMode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@17.6/manual/HDRP-Asset.html" rel="noopener noreferrer"&gt;HDRP: Dynamic Resolution and Use Mip Bias&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@17.6/manual/deep-learning-super-sampling-in-hdrp.html" rel="noopener noreferrer"&gt;HDRP: DLSS and Mip Bias&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@17.4/manual/Geometric-Specular-Anti-Aliasing.html" rel="noopener noreferrer"&gt;HDRP: Geometric Specular Anti-Aliasing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>graphics</category>
      <category>shaders</category>
    </item>
    <item>
      <title>Is LitMotion the Best Tween Library for Unity in 2026? A Practical Guide</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Tue, 18 Aug 2026 10:19:47 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/is-litmotion-the-best-tween-library-for-unity-in-2026-a-practical-guide-1k4e</link>
      <guid>https://dev.to/gamedevtoollab/is-litmotion-the-best-tween-library-for-unity-in-2026-a-practical-guide-1k4e</guid>
      <description>&lt;p&gt;DOTween has been the default answer for Unity tweening for years, and it is still a very strong library. But for a new code-driven project, I would no longer choose it automatically.&lt;/p&gt;

&lt;p&gt;After using LitMotion recently, I found its API especially comfortable: Transform values, UI values, and custom values all follow the same &lt;code&gt;Create -&amp;gt; With -&amp;gt; Bind&lt;/code&gt; pattern. LitMotion v2 also adds Sequence support, Inspector editing through LitMotion.Animation, debugging tools, and improved cancellation/control APIs.&lt;/p&gt;

&lt;p&gt;This article compares LitMotion, PrimeTween, and DOTween, then shows the parts of LitMotion I consider important in production: setup, &lt;code&gt;MotionHandle&lt;/code&gt;, object lifetime, &lt;code&gt;Time.timeScale&lt;/code&gt;, Sequence, UniTask, Punch/Shake, TextMeshPro, and migration design.&lt;/p&gt;

&lt;p&gt;The version information here was checked on &lt;strong&gt;July 28, 2026&lt;/strong&gt;. Recheck release pages if you read this much later.&lt;/p&gt;

&lt;h2&gt;
  
  
  My recommendation
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&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 project, animation logic mainly in C#&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;LitMotion&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Very sensitive to tween CPU/GC cost&lt;/td&gt;
&lt;td&gt;Benchmark &lt;strong&gt;LitMotion and PrimeTween&lt;/strong&gt; in your real workload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefer &lt;code&gt;Tween.Position()&lt;/code&gt;-style target APIs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;PrimeTween&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Want short Inspector-edited animations&lt;/td&gt;
&lt;td&gt;Evaluate &lt;strong&gt;LitMotion.Animation&lt;/strong&gt;, PrimeTween PRO, and DOTween Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing project already uses DOTween heavily&lt;/td&gt;
&lt;td&gt;Usually &lt;strong&gt;keep DOTween&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third-party assets depend on DOTween&lt;/td&gt;
&lt;td&gt;Keep DOTween, or strictly separate ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a new code-first project, &lt;strong&gt;LitMotion is currently my first candidate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It emphasizes low-allocation tween creation, uses the C# Job System and Burst, supports arbitrary values, and keeps a consistent API. It is MIT licensed and supports Unity 2021.3 or later.&lt;/p&gt;

&lt;p&gt;That does not mean your whole animation path becomes allocation-free. Capturing lambdas, strings, LINQ, async code, Canvas rebuilds, and whatever you do in the binding still matter.&lt;/p&gt;

&lt;p&gt;At the research date, LitMotion's latest release was v2.0.2. PrimeTween was also actively maintained, so I consider it a serious alternative rather than a secondary option.&lt;/p&gt;

&lt;h2&gt;
  
  
  LitMotion, PrimeTween, and DOTween
&lt;/h2&gt;

&lt;h3&gt;
  
  
  LitMotion
&lt;/h3&gt;

&lt;p&gt;LitMotion is a data-oriented tween library built around &lt;code&gt;LMotion&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;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&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="m"&gt;0.3f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutCubic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bind&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;alpha&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a value transition, add options, then bind it to a target. The same structure works for Transform, UI, TextMeshPro, and your own values.&lt;/p&gt;

&lt;p&gt;LitMotion v2 adds &lt;code&gt;LSequence&lt;/code&gt;, LitMotion.Animation, &lt;code&gt;TryCancel()&lt;/code&gt; / &lt;code&gt;TryComplete()&lt;/code&gt; style handle control, and LitMotion Debugger.&lt;/p&gt;

&lt;h3&gt;
  
  
  PrimeTween
&lt;/h3&gt;

&lt;p&gt;PrimeTween is also designed around modern, low-allocation usage, but its API feels more target-oriented.&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;Tween&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PositionY&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;endValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;duration&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="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InOutSine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Starting from &lt;code&gt;Tween.&lt;/code&gt; is very discoverable in an IDE. If you prefer choosing a target operation first, PrimeTween may feel more natural than LitMotion.&lt;/p&gt;

&lt;h3&gt;
  
  
  DOTween
&lt;/h3&gt;

&lt;p&gt;DOTween remains excellent when you already have production code and tools around 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;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DOMoveX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10f&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tweening&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutCubic&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It has a mature ecosystem, many shortcuts, strong Sequence support, and DOTween Pro. If your project already depends on those strengths and profiling shows no problem, migration can easily cost more than it saves.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about MagicTween?
&lt;/h3&gt;

&lt;p&gt;MagicTween's repository was archived in 2025 and points users toward LitMotion. I would not choose MagicTween for a new project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not choose from a benchmark alone
&lt;/h2&gt;

&lt;p&gt;The author's public &lt;a href="https://github.com/AnnulusGames/TweenPerformance" rel="noopener noreferrer"&gt;TweenPerformance&lt;/a&gt; project shows strong LitMotion results in its tested environment, including very low allocation during tween creation.&lt;/p&gt;

&lt;p&gt;But benchmark versions differ from the current libraries, the environment is specific, and real games also pay for bindings, Canvas work, closures, strings, async state machines, materials, and game-specific logic.&lt;/p&gt;

&lt;p&gt;Use benchmarks as a hint. Make the final decision from a Player build on your target hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing LitMotion
&lt;/h2&gt;

&lt;p&gt;For LitMotion v2.0.2, the package requirements are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unity 2021.3+&lt;/li&gt;
&lt;li&gt;Burst 1.6.0+&lt;/li&gt;
&lt;li&gt;Collections 1.5.1+&lt;/li&gt;
&lt;li&gt;Mathematics 1.0.1+&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add the package through Unity Package Manager. In a team project, pin a tag or commit.&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/annulusgames/LitMotion.git?path=src/LitMotion/Assets/LitMotion#v2.0.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;?path=...&lt;/code&gt; part comes before &lt;code&gt;#v2.0.2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Typical namespaces are:&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;LitMotion&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;LitMotion.Extensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;UniTask integration requires &lt;code&gt;com.cysharp.unitask&lt;/code&gt;. TextMeshPro integration requires TMP. If you use custom asmdefs, verify references to &lt;code&gt;LitMotion&lt;/code&gt;, &lt;code&gt;LitMotion.Extensions&lt;/code&gt;, UniTask, and Unity.TextMeshPro as needed.&lt;/p&gt;

&lt;p&gt;The snippets in this article are based on LitMotion v2.0.2's public API and source. I would still compile representative examples in a minimal Unity project before adopting them as team standards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core pattern: Create, configure, Bind
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LMotion.Create()&lt;/code&gt; returns a builder. The motion becomes useful when you configure it and finally bind 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;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&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="m"&gt;0.3f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutCubic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bind&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;alpha&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a Transform:&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;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&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;target&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;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.4f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutCubic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToPosition&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="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="n"&gt;target&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an arbitrary value:&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;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&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;volume&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="m"&gt;0.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;WithEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bind&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;volume&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="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="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A capturing lambda can allocate. If this is a hot path, use a state-passing overload 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="n"&gt;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&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;volume&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="m"&gt;0.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;Bind&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;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&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;self&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;x&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="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For built-in properties, dedicated &lt;code&gt;BindTo...()&lt;/code&gt; helpers are usually clearer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ease, delay, and loops
&lt;/h2&gt;

&lt;p&gt;Builder options compose naturally:&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;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&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="m"&gt;0.25f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.1f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutBack&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bind&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="k"&gt;value&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;List entrance effects are easy to stagger:&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;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="n"&gt;items&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;i&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;item&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="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="n"&gt;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&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="m"&gt;0.25f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithDelay&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="m"&gt;0.04f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutCubic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToLocalScaleXYZ&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;)&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="n"&gt;item&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For infinite loops:&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;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.95f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1.05f&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithLoops&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;LoopType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Yoyo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToLocalScaleXYZ&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="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="n"&gt;gameObject&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;-1&lt;/code&gt; means the motion does not end by itself, so always provide a lifetime through object destruction, a stored handle, or cancellation.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;MotionHandle&lt;/code&gt;: stop old animations before starting new ones
&lt;/h2&gt;

&lt;p&gt;Bindings return a &lt;code&gt;MotionHandle&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;void&lt;/span&gt; &lt;span class="nf"&gt;Replay&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryCancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&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="m"&gt;0.4f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutCubic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bind&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="k"&gt;value&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;TryComplete()&lt;/code&gt; when you want to apply the final value instead of stopping at the current one.&lt;/p&gt;

&lt;p&gt;The bigger design issue is &lt;strong&gt;property ownership&lt;/strong&gt;. If open/close, hover, selection, and click feedback all write the same Scale or Alpha, they can fight each other even if every individual tween is 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="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;MotionHandle&lt;/span&gt; &lt;span class="n"&gt;fadeHandle&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;SetVisible&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;visible&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fadeHandle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryCancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;fadeHandle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&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;canvasGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;visible&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="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="m"&gt;0.2f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutCubic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToAlpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canvasGroup&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="n"&gt;gameObject&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;Starting from the current value also avoids a jump when the animation reverses halfway through.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;AddTo(gameObject)&lt;/code&gt; and CancellationToken are different tools
&lt;/h2&gt;

&lt;p&gt;For UI and temporary objects, I normally tie a motion to its owner:&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;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&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="m"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bind&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="k"&gt;value&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="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="n"&gt;gameObject&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;AddTo(gameObject)&lt;/code&gt; cancels the motion when the GameObject is destroyed. It is lifetime ownership, not async-flow control.&lt;/p&gt;

&lt;p&gt;LitMotion v2.0.2's &lt;code&gt;ToUniTask(token)&lt;/code&gt; uses &lt;code&gt;CancelBehavior.Cancel&lt;/code&gt; with &lt;code&gt;cancelAwaitOnMotionCanceled: true&lt;/code&gt;. If you want the intent visible in code, write it 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;await&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUniTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CancelBehavior&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="n"&gt;cancelAwaitOnMotionCanceled&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;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If token cancellation should move the motion to its end value, use &lt;code&gt;CancelBehavior.Complete&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Decide whether a screen token, GameObject lifetime, explicit handle, or caller token owns the animation. Do not add all of them blindly and assume they mean the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  UI animation while &lt;code&gt;Time.timeScale == 0&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Pause menus often still need animation after gameplay time is stopped.&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;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&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="m"&gt;0.2f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithScheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MotionScheduler&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;WithEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutCubic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToAlpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canvasGroup&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="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful schedulers include normal &lt;code&gt;Update&lt;/code&gt;, &lt;code&gt;UpdateIgnoreTimeScale&lt;/code&gt;, &lt;code&gt;FixedUpdate&lt;/code&gt;, and the &lt;code&gt;PreLateUpdate&lt;/code&gt; / &lt;code&gt;PostLateUpdate&lt;/code&gt; families.&lt;/p&gt;

&lt;p&gt;The scheduler also changes ordering relative to other scripts, so verify camera, layout, and physics interactions instead of treating this as only a time-scale switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sequence for fixed timelines
&lt;/h2&gt;

&lt;p&gt;LitMotion v2 provides &lt;code&gt;LSequence&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;sequenceHandle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LSequence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&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="m"&gt;0.25f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutCubic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToAlpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canvasGroup&lt;/span&gt;&lt;span class="p"&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="n"&gt;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.9f&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="m"&gt;0.25f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutBack&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToLocalScaleXYZ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.15f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&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;panel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localPosition&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;panel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localPosition&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="m"&gt;20f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="m"&gt;0.2f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithLoops&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="n"&gt;LoopType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Yoyo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToLocalPositionY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;))&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;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Append()&lt;/code&gt; is sequential, &lt;code&gt;Join()&lt;/code&gt; is parallel, &lt;code&gt;Insert()&lt;/code&gt; places a motion at a specific time, and &lt;code&gt;AppendInterval()&lt;/code&gt; adds a wait. &lt;code&gt;Run()&lt;/code&gt; starts the sequence.&lt;/p&gt;

&lt;p&gt;If initial values are being applied too early while assembling a sequence, consider &lt;code&gt;WithImmediateBind(false)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sequence is best for a fixed timeline. Input waits, network waits, branching, and complex cancellation are usually clearer in async code or a state machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  UniTask: &lt;code&gt;await handle&lt;/code&gt; and &lt;code&gt;ToUniTask()&lt;/code&gt; do not cancel the same way
&lt;/h2&gt;

&lt;p&gt;LitMotion supports directly awaiting a &lt;code&gt;MotionHandle&lt;/code&gt;, but in v2.0.2 &lt;code&gt;await handle;&lt;/code&gt; resumes on either completion or cancellation, and &lt;code&gt;MotionAwaiter.GetResult()&lt;/code&gt; does not throw.&lt;/p&gt;

&lt;p&gt;If you need CancellationToken integration or want motion cancellation to propagate as async cancellation, use &lt;code&gt;ToUniTask()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A UI animation that can be triggered repeatedly can share one linked CTS so the next play request cancels the previous one:&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;CancellationTokenSource&lt;/span&gt; &lt;span class="n"&gt;playCts&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;UniTask&lt;/span&gt; &lt;span class="nf"&gt;PlayAsync&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;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;playCts&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Cancel&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;currentCts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateLinkedTokenSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;playCts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentCts&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;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentCts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&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;fade&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&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;canvasGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alpha&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="m"&gt;0.25f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToAlpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canvasGroup&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="n"&gt;gameObject&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;scale&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&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;panel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localScale&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;1f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.3f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutBack&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToLocalScaleXYZ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;panel&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="n"&gt;gameObject&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;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;ToCancelableUniTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nf"&gt;ToCancelableUniTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ReferenceEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playCts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentCts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;playCts&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;currentCts&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;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt; &lt;span class="nf"&gt;ToCancelableUniTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;MotionHandle&lt;/span&gt; &lt;span class="n"&gt;handle&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;token&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;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUniTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;CancelBehavior&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="n"&gt;cancelAwaitOnMotionCanceled&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;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&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 a new call cancels the previous &lt;code&gt;PlayAsync()&lt;/code&gt;, the previous caller observes cancellation. If that is normal UI control flow, handle it at a clear boundary:&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;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;resultPanelAnimation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PlayAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&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;System&lt;/span&gt;&lt;span class="p"&gt;.&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="c1"&gt;// Re-entry, screen transition, or destruction is normal here.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important point is not to hide cancellation semantics. Decide where cancellation becomes a normal result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Punch and Shake
&lt;/h2&gt;

&lt;p&gt;Punch is useful for button feedback:&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;baseScale&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buttonTransform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localScale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Punch&lt;/span&gt;&lt;span class="p"&gt;.&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;baseScale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.15f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.3f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithFrequency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithDampingRatio&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToLocalScale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buttonTransform&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="n"&gt;buttonTransform&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second argument is &lt;strong&gt;strength&lt;/strong&gt;, not an end value. Do not pass &lt;code&gt;Vector3.zero&lt;/code&gt; as the start value unless you intentionally want to oscillate around zero scale.&lt;/p&gt;

&lt;p&gt;Shake works similarly:&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;basePosition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cameraRoot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localPosition&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shake&lt;/span&gt;&lt;span class="p"&gt;.&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;basePosition&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;Vector3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;8f&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;0.35f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithFrequency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithDampingRatio&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithRandomSeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;123&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToLocalPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cameraRoot&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="n"&gt;cameraRoot&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For cameras, a dedicated shake-offset transform is often safer than letting Cinemachine, follow code, recoil, and shake all write the same transform.&lt;/p&gt;

&lt;h2&gt;
  
  
  TextMeshPro integration
&lt;/h2&gt;

&lt;p&gt;LitMotion can animate fixed-size strings:&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;LMotion&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="nf"&gt;Create128Bytes&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="s"&gt;"&amp;lt;color=#FFD54F&amp;gt;MISSION COMPLETE&amp;lt;/color&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="m"&gt;1.2f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithRichText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithScrambleChars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ScrambleMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Uppercase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messageText&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="n"&gt;messageText&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;128&lt;/code&gt; is a &lt;strong&gt;byte capacity&lt;/strong&gt;, not a character count. Localized Japanese text, emoji, and RichText tags can consume more space than a short English sample. Choose capacity from the longest production string.&lt;/p&gt;

&lt;p&gt;Numeric binding is also convenient:&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;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&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;9999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.8f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutCubic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scoreText&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="n"&gt;scoreText&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Formatted binding is supported too:&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;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&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;100000f&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scoreText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"{0:N2}"&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="n"&gt;scoreText&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The standard formatted path uses &lt;code&gt;string.Format()&lt;/code&gt;, so it can allocate. If this is a hot path, consider the documented ZString integration and profile the full UI update cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  LitMotion.Animation for Inspector workflows
&lt;/h2&gt;

&lt;p&gt;LitMotion.Animation is a separate package:&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/annulusgames/LitMotion.git?path=src/LitMotion/Assets/LitMotion.Animation#v2.0.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When using Git URL dependencies, I prefer pinning both core and animation packages to the same tag explicitly:&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;"dependencies"&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;"com.annulusgames.lit-motion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/annulusgames/LitMotion.git?path=src/LitMotion/Assets/LitMotion#v2.0.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"com.annulusgames.lit-motion.animation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/annulusgames/LitMotion.git?path=src/LitMotion/Assets/LitMotion.Animation#v2.0.2"&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;It supports Edit Mode and Play Mode previews and is useful for short Prefab-specific animation tuning.&lt;/p&gt;

&lt;p&gt;I would still define a team boundary: keep shared duration, ease, and cancellation conventions in code or configuration, and leave only local presentation tuning to the Inspector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common production mistakes
&lt;/h2&gt;

&lt;p&gt;A few problems matter more than the exact tween API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting to bind:&lt;/strong&gt; &lt;code&gt;LMotion.Create(...).WithEase(...)&lt;/code&gt; is only a builder until you call &lt;code&gt;Bind()&lt;/code&gt; / &lt;code&gt;BindTo...()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple motions write one property:&lt;/strong&gt; separate ownership or cancel the previous handle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infinite loops have no shutdown:&lt;/strong&gt; combine them with object lifetime, a handle, or cancellation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pause UI stops:&lt;/strong&gt; use an ignore-time-scale scheduler only where appropriate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Zero allocation" is interpreted too broadly:&lt;/strong&gt; closures, strings, async code, and Unity systems still allocate or consume CPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything goes into Sequence:&lt;/strong&gt; use async/state machines for branching and external waits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LitMotion Debugger (&lt;code&gt;Window &amp;gt; LitMotion Debugger&lt;/code&gt;) is useful for finding duplicate motions and missing cancellation. Use it in Editor Play Mode; profile final CPU/GC with the debugger disabled in a Player build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating from DOTween
&lt;/h2&gt;

&lt;p&gt;Simple cases are easy to rewrite:&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;// DOTween&lt;/span&gt;
&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DOMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetPosition&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tweening&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutCubic&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="c1"&gt;// LitMotion&lt;/span&gt;
&lt;span class="n"&gt;LMotion&lt;/span&gt;&lt;span class="p"&gt;.&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;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;targetPosition&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LitMotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutCubic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindToPosition&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="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="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real migration risk is behavior around the one-liner. Review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kill vs cancel behavior;&lt;/li&gt;
&lt;li&gt;whether cancellation keeps the current value or applies the final value;&lt;/li&gt;
&lt;li&gt;Sequence behavior;&lt;/li&gt;
&lt;li&gt;update timing and &lt;code&gt;Time.timeScale&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;relative tweens;&lt;/li&gt;
&lt;li&gt;DOTween Pro data;&lt;/li&gt;
&lt;li&gt;third-party integrations and internal wrappers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Migrate feature by feature, not library-wide, and never let two tween systems own the same property at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adoption checklist
&lt;/h2&gt;

&lt;p&gt;Before standardizing LitMotion in a team, I would verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unity/Burst/Collections/Mathematics requirements;&lt;/li&gt;
&lt;li&gt;custom asmdef references for LitMotion, extensions, UniTask, and TMP;&lt;/li&gt;
&lt;li&gt;Git dependencies pinned to a tag or commit;&lt;/li&gt;
&lt;li&gt;current LitMotion Releases and PrimeTween Changelog;&lt;/li&gt;
&lt;li&gt;representative snippets compiled in a minimal Unity project;&lt;/li&gt;
&lt;li&gt;third-party assets and internal tooling dependencies;&lt;/li&gt;
&lt;li&gt;cancellation rules for destruction, reopening, and repeated input;&lt;/li&gt;
&lt;li&gt;which animations ignore &lt;code&gt;Time.timeScale&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Sequence vs async/await conventions;&lt;/li&gt;
&lt;li&gt;Player-build CPU and GC on target hardware.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also test cancellation halfway through, object destruction, rapid reversal, and repeated clicks—not only normal completion.&lt;/p&gt;

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

&lt;p&gt;For a new Unity project in 2026 where tween logic is mainly written in C#, &lt;strong&gt;LitMotion is the first library I would evaluate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Its &lt;code&gt;Create -&amp;gt; With -&amp;gt; Bind&lt;/code&gt; model is consistent across Transform values, arbitrary data, TextMeshPro, Punch/Shake, Sequence, and UniTask integration. LitMotion v2 also addresses many of the practical features that once made mature alternatives easier to justify by default.&lt;/p&gt;

&lt;p&gt;PrimeTween remains a strong alternative, especially if target-oriented APIs fit your team better. DOTween is still the pragmatic choice for projects that already have a working DOTween ecosystem.&lt;/p&gt;

&lt;p&gt;Whichever library you choose, the production problems are usually property ownership, re-entry, cancellation, object lifetime, time scale, and the code around the tween—not the number of characters in the tween call.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Official docs and source
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://annulusgames.github.io/LitMotion/" rel="noopener noreferrer"&gt;LitMotion documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/annulusgames/LitMotion" rel="noopener noreferrer"&gt;LitMotion GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/annulusgames/LitMotion/blob/v2.0.2/src/LitMotion/Assets/LitMotion/Runtime/Extensions/TextMeshPro/LitMotionTextMeshProExtensions.cs" rel="noopener noreferrer"&gt;LitMotion v2.0.2 TextMeshPro extensions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/annulusgames/LitMotion/blob/v2.0.2/src/LitMotion/Assets/LitMotion/Runtime/External/UniTask/LitMotionUniTaskExtensions.cs" rel="noopener noreferrer"&gt;LitMotion v2.0.2 UniTask extensions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/KyryloKuzyk/PrimeTween" rel="noopener noreferrer"&gt;PrimeTween GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dotween.demigiant.com/documentation.php" rel="noopener noreferrer"&gt;DOTween documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Releases and benchmarks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/annulusgames/LitMotion/releases" rel="noopener noreferrer"&gt;LitMotion Releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/KyryloKuzyk/PrimeTween/blob/main/changelog.md" rel="noopener noreferrer"&gt;PrimeTween Changelog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/AnnulusGames/TweenPerformance" rel="noopener noreferrer"&gt;TweenPerformance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/upm-git.html" rel="noopener noreferrer"&gt;Unity Package Manager Git dependencies&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>gamedev</category>
      <category>performance</category>
    </item>
    <item>
      <title>WAV Is Not Automatically High Quality in Unity: PCM, ADPCM, Vorbis, and Load Types</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Fri, 14 Aug 2026 11:52:56 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/wav-is-not-automatically-high-quality-in-unity-pcm-adpcm-vorbis-and-load-types-49dh</link>
      <guid>https://dev.to/gamedevtoollab/wav-is-not-automatically-high-quality-in-unity-pcm-adpcm-vorbis-and-load-types-49dh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Scope&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
This article is based mainly on the Unity 6.5 documentation available on August 4, 2026. I did not run device benchmarks or controlled listening tests. Treat every recommendation as a starting point, then verify it in a Player build on the target device with the actual maximum voice count, the Audio Profiler, and representative speakers or headphones.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  WAV is only the source
&lt;/h2&gt;

&lt;p&gt;Importing a high-quality WAV file does not guarantee that the shipped game plays the same uncompressed waveform.&lt;/p&gt;

&lt;p&gt;WAV is an &lt;strong&gt;input container&lt;/strong&gt;. Unity can convert it to PCM, ADPCM, Vorbis, MP3, or another platform-supported format. &lt;code&gt;Load Type&lt;/code&gt; then decides whether Unity keeps decoded samples in memory, keeps compressed data in memory and decodes during playback, or reads the clip progressively from storage.&lt;/p&gt;

&lt;p&gt;The runtime result depends on the combination:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source file
× Compression Format
× Quality / Sample Rate / channels
× Load Type
× Preload Audio Data / Load In Background
× simultaneous playback
× target platform
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why “WAV means high quality,” “Vorbis always saves memory,” and “Streaming is always lighter” are unreliable rules.&lt;/p&gt;

&lt;p&gt;In this article, &lt;strong&gt;BGM&lt;/strong&gt; means background music and &lt;strong&gt;SFX&lt;/strong&gt; means sound effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical starting points
&lt;/h2&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;Format&lt;/th&gt;
&lt;th&gt;Load Type&lt;/th&gt;
&lt;th&gt;Channels&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;Long BGM&lt;/td&gt;
&lt;td&gt;Vorbis&lt;/td&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;Stereo&lt;/td&gt;
&lt;td&gt;The start is predictable, and full decoded residency is avoided&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short jingle&lt;/td&gt;
&lt;td&gt;Vorbis or PCM&lt;/td&gt;
&lt;td&gt;Compressed In Memory or Decompress On Load&lt;/td&gt;
&lt;td&gt;Stereo&lt;/td&gt;
&lt;td&gt;Streaming overhead may not be worthwhile for a few seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI click&lt;/td&gt;
&lt;td&gt;PCM&lt;/td&gt;
&lt;td&gt;Decompress On Load&lt;/td&gt;
&lt;td&gt;Depends&lt;/td&gt;
&lt;td&gt;It should follow an unpredictable input without runtime I/O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Footstep, weapon, impact&lt;/td&gt;
&lt;td&gt;ADPCM or PCM&lt;/td&gt;
&lt;td&gt;Start with Decompress On Load&lt;/td&gt;
&lt;td&gt;Mono&lt;/td&gt;
&lt;td&gt;It should follow gameplay events without playback-time decoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium SFX&lt;/td&gt;
&lt;td&gt;Vorbis&lt;/td&gt;
&lt;td&gt;Compressed In Memory&lt;/td&gt;
&lt;td&gt;Depends&lt;/td&gt;
&lt;td&gt;A middle ground between decoded memory and CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical dialogue&lt;/td&gt;
&lt;td&gt;Vorbis&lt;/td&gt;
&lt;td&gt;Compressed In Memory&lt;/td&gt;
&lt;td&gt;Mono candidate&lt;/td&gt;
&lt;td&gt;The next line can be prefetched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long narration&lt;/td&gt;
&lt;td&gt;Vorbis&lt;/td&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;Mono candidate&lt;/td&gt;
&lt;td&gt;Several minutes do not need to be fully decoded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Battle bark&lt;/td&gt;
&lt;td&gt;PCM or ADPCM&lt;/td&gt;
&lt;td&gt;Decompress On Load&lt;/td&gt;
&lt;td&gt;Mono candidate&lt;/td&gt;
&lt;td&gt;Combat feedback should start immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are baselines for native builds, not final presets. Web builds require a separate policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep three decisions separate
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Source file
&lt;/h3&gt;

&lt;p&gt;The file under &lt;code&gt;Assets&lt;/code&gt;: WAV, AIFF, MP3, Ogg Vorbis, FLAC, and so on. Prefer an uncompressed or lossless production master. Converting MP3 to WAV does not restore discarded information, and encoding that WAV to Vorbis adds another lossy generation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compression Format
&lt;/h3&gt;

&lt;p&gt;PCM, ADPCM, and Vorbis affect build size, compression artifacts, compressed-data size, and decoding cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Load Type
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Decompress On Load&lt;/code&gt;, &lt;code&gt;Compressed In Memory&lt;/code&gt;, and &lt;code&gt;Streaming&lt;/code&gt; decide where data lives and when decoding and storage access occur.&lt;/p&gt;

&lt;p&gt;Compression Format and Load Type are independent. A Vorbis clip using &lt;code&gt;Decompress On Load&lt;/code&gt; is decoded during loading, so “Vorbis” alone does not imply low runtime memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  PCM, ADPCM, and Vorbis
&lt;/h2&gt;

&lt;h3&gt;
  
  
  PCM
&lt;/h3&gt;

&lt;p&gt;PCM is uncompressed. It avoids lossy artifacts and playback-time decompression, making it a simple choice for tiny, timing-critical sounds.&lt;/p&gt;

&lt;p&gt;A rough payload estimate is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bytes per second
= sample rate × bytes per sample × channel count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 44.1 kHz, 16-bit stereo clip lasting 60 seconds is about 10.1 MiB before Unity-specific buffers. PCM is easy to justify for a click, but expensive as a blanket policy for music.&lt;/p&gt;

&lt;h3&gt;
  
  
  ADPCM
&lt;/h3&gt;

&lt;p&gt;ADPCM is smaller than PCM and generally cheaper to decode than Vorbis. Unity documents roughly 3.5:1 compression compared with PCM and suggests footsteps, impacts, and weapon sounds as examples.&lt;/p&gt;

&lt;p&gt;It can expose artifacts in tonal or smooth content. Also, &lt;code&gt;ADPCM + Decompress On Load&lt;/code&gt; still needs decoded sample memory after loading; the compression ratio does not produce the same reduction in runtime memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vorbis
&lt;/h3&gt;

&lt;p&gt;Vorbis offers a quality setting that trades size against audible loss. It is a common candidate for BGM, dialogue, narration, ambience, and medium-to-long effects.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;Compressed In Memory&lt;/code&gt;, decoding contributes to mixer work. With &lt;code&gt;Streaming&lt;/code&gt;, work moves to the streaming path. Test difficult material such as cymbals, sibilance, reverb tails, quiet ambience, and high-frequency synthetic sounds.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;PCM&lt;/th&gt;
&lt;th&gt;ADPCM&lt;/th&gt;
&lt;th&gt;Vorbis&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compression&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Lightweight fixed compression&lt;/td&gt;
&lt;td&gt;High-ratio lossy compression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build size&lt;/td&gt;
&lt;td&gt;Large&lt;/td&gt;
&lt;td&gt;About one third of PCM as a guideline&lt;/td&gt;
&lt;td&gt;Often smaller than ADPCM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playback CPU&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Higher decoding cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good starting use&lt;/td&gt;
&lt;td&gt;Tiny critical SFX&lt;/td&gt;
&lt;td&gt;Footsteps, impacts, weapons&lt;/td&gt;
&lt;td&gt;BGM, voice, medium/long SFX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main risk&lt;/td&gt;
&lt;td&gt;Long clips consume space and memory&lt;/td&gt;
&lt;td&gt;Artifacts in tonal content&lt;/td&gt;
&lt;td&gt;Many decoders can raise CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The three Load Types
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Decompress On Load
&lt;/h3&gt;

&lt;p&gt;Unity decodes the clip while loading it and keeps decoded samples available. It suits short clips where the decoded total is small and playback-time work or startup uncertainty matters more than memory.&lt;/p&gt;

&lt;p&gt;Unity 6.5 gives rough warnings that decoded Vorbis can require about ten times the compressed size and ADPCM about 3.5 times. These are planning guidelines, not fixed measurements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compressed In Memory
&lt;/h3&gt;

&lt;p&gt;Unity keeps compressed data in memory and decodes it during playback. This often fits medium SFX and dialogue libraries that are too large to keep fully decoded but too short to stream individually.&lt;/p&gt;

&lt;p&gt;The trade-off is mixer-side CPU. Test the real worst case with effects, spatialization, pitch changes, and overlapping voices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming
&lt;/h3&gt;

&lt;p&gt;Unity reads compressed data progressively from storage and decodes through the streaming path. It is a natural baseline for long BGM, narration, and ambience.&lt;/p&gt;

&lt;p&gt;Streaming is not free. Unity 6.5 lists approximately 200 KB of overhead per streaming clip. Hundreds of short streams can therefore be worse than a few long ones. Profile &lt;code&gt;Streaming CPU&lt;/code&gt;, &lt;code&gt;Streaming File Memory&lt;/code&gt;, and &lt;code&gt;Streaming Decode Memory&lt;/code&gt; in a Player build.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Load Type&lt;/th&gt;
&lt;th&gt;Memory behavior&lt;/th&gt;
&lt;th&gt;Playback work&lt;/th&gt;
&lt;th&gt;Typical use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Decompress On Load&lt;/td&gt;
&lt;td&gt;Holds decoded samples&lt;/td&gt;
&lt;td&gt;Low decoding work during playback&lt;/td&gt;
&lt;td&gt;Short, timing-critical SFX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compressed In Memory&lt;/td&gt;
&lt;td&gt;Holds compressed data&lt;/td&gt;
&lt;td&gt;Decodes on the mixer path&lt;/td&gt;
&lt;td&gt;Medium SFX and dialogue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;Avoids full decoded residency&lt;/td&gt;
&lt;td&gt;Progressive I/O and decoding&lt;/td&gt;
&lt;td&gt;BGM and long voice&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Choose by playback timing, not only duration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Unpredictable gameplay sounds
&lt;/h3&gt;

&lt;p&gt;A UI click, attack, perfect dodge, landing, hit confirmation, or &lt;code&gt;Animation Event&lt;/code&gt; cannot always be scheduled ahead. Its timing is part of the control feedback.&lt;/p&gt;

&lt;p&gt;Streaming introduces storage access, stream initialization, and buffering. This does not mean every streamed sound is always audibly late. It means there is little reason to add that I/O risk to a tiny sound whose main job is immediate response.&lt;/p&gt;

&lt;p&gt;Start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Decompress On Load
+ Preload Audio Data enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Preload is disabled, call &lt;code&gt;LoadAudioData()&lt;/code&gt; early and verify &lt;code&gt;loadState&lt;/code&gt; before gameplay. &lt;code&gt;Decompress On Load&lt;/code&gt; alone does not guarantee that the first &lt;code&gt;Play()&lt;/code&gt; call performs no loading work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Predictable audio
&lt;/h3&gt;

&lt;p&gt;BGM, narration, cutscene audio, and the next dialogue line normally have preparation time. Long clips can stream; shorter voice lines can stay compressed in memory and be prefetched.&lt;/p&gt;

&lt;p&gt;For music and synchronized stems, use &lt;code&gt;AudioSettings.dspTime&lt;/code&gt; and &lt;code&gt;PlayScheduled()&lt;/code&gt; rather than frame timing. &lt;code&gt;loadState == Loaded&lt;/code&gt; is only a minimum precondition: it does not guarantee that storage and decoding remain safe during scene loading, AssetBundle work, crossfades, or long playback.&lt;/p&gt;

&lt;h2&gt;
  
  
  BGM
&lt;/h2&gt;

&lt;p&gt;For music lasting several minutes, start with &lt;code&gt;Vorbis + Streaming + Stereo&lt;/code&gt;. Begin loading before the transition, check readiness, and schedule precise starts or crossfades. Test loop boundaries, two-track overlap, scene loading, slower storage, long playback, and suspend/resume.&lt;/p&gt;

&lt;p&gt;Do not apply the same rule to a three-second logo sound. Compare &lt;code&gt;Vorbis + Compressed In Memory&lt;/code&gt; with &lt;code&gt;PCM/Vorbis + Decompress On Load&lt;/code&gt;; per-stream overhead may cost more than the clip justifies.&lt;/p&gt;

&lt;p&gt;Adaptive music must be measured at the maximum stem count. Four outgoing stems overlapping four incoming stems can briefly create eight streams. Synchronized assets should share the intended sample rate, duration, and loop boundaries and be ready before the same DSP-time start is scheduled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Addressables and AssetBundles are a separate I/O layer
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Load Type = Streaming&lt;/code&gt; describes the AudioClip. It does not complete the distribution design.&lt;/p&gt;

&lt;p&gt;Bundle grouping, bundle compression, local or remote placement, caching, pre-download, custom encryption, and concurrent loading all affect startup and I/O contention. AudioClip Vorbis compression and AssetBundle LZ4/LZMA compression are separate layers.&lt;/p&gt;

&lt;p&gt;A remote BGM flow may look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Download required dependencies
→ LoadAssetAsync&amp;lt;AudioClip&amp;gt;() and keep the handle
→ wait for the required audio-data preparation
→ schedule or start playback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pre-downloading a bundle does not by itself mean that the AudioClip asset and its audio data are ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  SFX
&lt;/h2&gt;

&lt;h3&gt;
  
  
  UI and action feedback
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;PCM + Decompress On Load&lt;/code&gt; as the baseline for tiny clicks, confirm/cancel sounds, attack starts, perfect-input feedback, and hit confirmation. Ensure the audio data is loaded before the player can trigger them.&lt;/p&gt;

&lt;p&gt;If hundreds are resident, inspect the decoded total. Mono is a candidate when stereo carries no information, but do not automatically collapse deliberately stereo UI sounds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Footsteps, impacts, and weapons
&lt;/h3&gt;

&lt;p&gt;Short, noisy, frequently repeated sounds are good ADPCM candidates. Start with &lt;code&gt;ADPCM + Decompress On Load + Mono&lt;/code&gt;, then evaluate the worst-case soundscape rather than one isolated clip: maximum enemies, surface variations, BGM, voice, effects, and the minimum-spec device.&lt;/p&gt;

&lt;h3&gt;
  
  
  Medium effects
&lt;/h3&gt;

&lt;p&gt;Mechanical loops and cinematic effects lasting several to tens of seconds often justify testing &lt;code&gt;Vorbis + Compressed In Memory&lt;/code&gt;. It reduces decoded residency but spends DSP CPU, so maximum concurrency decides whether it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Voice
&lt;/h2&gt;

&lt;p&gt;Typical dialogue is usually a mono candidate unless stereo carries real information such as binaural recording or intentional movement. Listen for phase and normalization changes when using Force To Mono.&lt;/p&gt;

&lt;p&gt;For lines lasting a few to several seconds, start with &lt;code&gt;Mono + Vorbis + Compressed In Memory&lt;/code&gt;. If Preload is off, call &lt;code&gt;LoadAudioData()&lt;/code&gt; while the current line plays and check &lt;code&gt;loadState&lt;/code&gt; before advancing.&lt;/p&gt;

&lt;p&gt;With Addressables, release responsibility should belong to the layer that owns the AudioClip. Keep the handle while an &lt;code&gt;AudioSource&lt;/code&gt;, pool, scheduled playback, Timeline/Playable, or next-line queue still references it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;optionally pre-download the bundle
→ LoadAssetAsync&amp;lt;AudioClip&amp;gt;() and keep the handle
→ LoadAudioData() if required, then verify loadState
→ play the clip
→ finish playback and clear references or schedules
→ UnloadAudioData() if appropriate
→ release the handle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For several minutes of narration, use &lt;code&gt;Mono + Vorbis + Streaming&lt;/code&gt; as the baseline and test interruption, seeking, suspend/resume, and long-session stability.&lt;/p&gt;

&lt;p&gt;Battle barks behave more like SFX. Combat shouts and damage reactions should usually be prepared as &lt;code&gt;PCM/ADPCM + Decompress On Load&lt;/code&gt; because delayed playback weakens feedback.&lt;/p&gt;

&lt;p&gt;When runtime code needs &lt;code&gt;AudioClip.GetData()&lt;/code&gt;, Unity 6.5 requires compressed audio to use &lt;code&gt;Decompress On Load&lt;/code&gt;, and Streaming clips are unavailable through that path. For long streamed voice, generate visemes, amplitude envelopes, or subtitle timing offline and read that lightweight data at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Channels, sample rate, preload, and background loading
&lt;/h2&gt;

&lt;p&gt;Converting stereo to mono roughly halves the sample count. Point-source 3D effects and typical dialogue are common candidates; music, wide ambience, and binaural content should normally remain stereo.&lt;/p&gt;

&lt;p&gt;Begin with &lt;code&gt;Preserve Sample Rate&lt;/code&gt; or &lt;code&gt;Optimize Sample Rate&lt;/code&gt;. Override only after a category creates a meaningful size problem, then listen carefully to sibilance, metallic transients, high electronic tones, airy ambience, and clips pitched upward.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Preload Audio Data&lt;/code&gt; prepares audio data with the asset according to the import configuration. When disabled, the first &lt;code&gt;Play()&lt;/code&gt; or &lt;code&gt;PlayOneShot()&lt;/code&gt; can start loading unless runtime code explicitly loads it first. Disabling Preload is not an optimization by itself; it moves responsibility into code.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Load In Background&lt;/code&gt; can reduce main-thread blocking, but it does not guarantee immediate playback. A request made before completion can wait. Prefetch sounds with a required start time and use DSP scheduling for synchronization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical presets
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Preset&lt;/th&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Load Type&lt;/th&gt;
&lt;th&gt;Preload&lt;/th&gt;
&lt;th&gt;Mono&lt;/th&gt;
&lt;th&gt;Main use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BGM_Long&lt;/td&gt;
&lt;td&gt;Vorbis&lt;/td&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;Usually Off&lt;/td&gt;
&lt;td&gt;Off&lt;/td&gt;
&lt;td&gt;Long-form BGM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BGM_Short&lt;/td&gt;
&lt;td&gt;Vorbis&lt;/td&gt;
&lt;td&gt;Compressed In Memory&lt;/td&gt;
&lt;td&gt;Project-dependent&lt;/td&gt;
&lt;td&gt;Off&lt;/td&gt;
&lt;td&gt;Jingles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SFX_UI&lt;/td&gt;
&lt;td&gt;PCM&lt;/td&gt;
&lt;td&gt;Decompress On Load&lt;/td&gt;
&lt;td&gt;On&lt;/td&gt;
&lt;td&gt;Depends&lt;/td&gt;
&lt;td&gt;UI feedback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SFX_Frequent&lt;/td&gt;
&lt;td&gt;ADPCM&lt;/td&gt;
&lt;td&gt;Decompress On Load&lt;/td&gt;
&lt;td&gt;On&lt;/td&gt;
&lt;td&gt;Candidate&lt;/td&gt;
&lt;td&gt;Footsteps and impacts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SFX_Medium&lt;/td&gt;
&lt;td&gt;Vorbis&lt;/td&gt;
&lt;td&gt;Compressed In Memory&lt;/td&gt;
&lt;td&gt;Project-dependent&lt;/td&gt;
&lt;td&gt;Depends&lt;/td&gt;
&lt;td&gt;Medium effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice_Dialogue&lt;/td&gt;
&lt;td&gt;Vorbis&lt;/td&gt;
&lt;td&gt;Compressed In Memory&lt;/td&gt;
&lt;td&gt;Usually Off with prefetch&lt;/td&gt;
&lt;td&gt;Candidate&lt;/td&gt;
&lt;td&gt;Dialogue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice_Bark&lt;/td&gt;
&lt;td&gt;PCM/ADPCM&lt;/td&gt;
&lt;td&gt;Decompress On Load&lt;/td&gt;
&lt;td&gt;On&lt;/td&gt;
&lt;td&gt;Candidate&lt;/td&gt;
&lt;td&gt;Combat voice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice_Long&lt;/td&gt;
&lt;td&gt;Vorbis&lt;/td&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;Usually Off&lt;/td&gt;
&lt;td&gt;Candidate&lt;/td&gt;
&lt;td&gt;Narration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;“Project-dependent” means the loading and distribution design decides. “Candidate” means a reasonable first comparison, not a rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Import automation: unverified template
&lt;/h2&gt;

&lt;p&gt;Manual settings drift in a large project. &lt;code&gt;AssetPostprocessor.OnPreprocessAudio()&lt;/code&gt; can encode a folder policy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; This follows the public Unity 6.5 API shape but was not compiled or reimport-tested for this article. Validate compilation, target and non-target folders, reimport behavior, and Inspector results in a test project before adoption.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&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;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;ProjectAudioImportPolicy&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AssetPostprocessor&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;uint&lt;/span&gt; &lt;span class="n"&gt;PolicyVersion&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;override&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="nf"&gt;GetVersion&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;PolicyVersion&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;OnPreprocessAudio&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;importer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AudioImporter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;assetImporter&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;settings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaultSampleSettings&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="n"&gt;sampleRateSetting&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AudioSampleRateSetting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PreserveSampleRate&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;IsUnder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Assets/Audio/BGM/Long/"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;forceToMono&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="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loadInBackground&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preloadAudioData&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="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loadType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AudioClipLoadType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Streaming&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="n"&gt;compressionFormat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AudioCompressionFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Vorbis&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="n"&gt;quality&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.8f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&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;IsUnder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Assets/Audio/SFX/UI/"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;forceToMono&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="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loadInBackground&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="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preloadAudioData&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loadType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AudioClipLoadType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DecompressOnLoad&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="n"&gt;compressionFormat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AudioCompressionFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PCM&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&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;IsUnder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Assets/Audio/Voice/Dialogue/"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// This template does not enforce a Normalize policy.&lt;/span&gt;
            &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;forceToMono&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;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loadInBackground&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preloadAudioData&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="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loadType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AudioClipLoadType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompressedInMemory&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="n"&gt;compressionFormat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AudioCompressionFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Vorbis&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="n"&gt;quality&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.7f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&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;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaultSampleSettings&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="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="nf"&gt;IsUnder&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;folder&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;assetPath&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="n"&gt;folder&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;Ordinal&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 quality values are comparison starting points, not universal recommendations. Perform level-matched A/B listening and adjust per platform.&lt;/p&gt;

&lt;p&gt;The template changes only three folders. Platform overrides can use &lt;code&gt;SetOverrideSampleSettings()&lt;/code&gt;, but its return value should be checked because unsupported combinations can fail. It also does not force Normalize; verify that policy through Presets, the Inspector, or version-tested automation.&lt;/p&gt;

&lt;p&gt;Automation does not replace judgment. It consistently applies a decision the team has already validated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform overrides and Web
&lt;/h2&gt;

&lt;p&gt;Windows, Android, iOS, and Web have different decoding performance, memory budgets, storage behavior, and implementation constraints. Use Default as a baseline, then override platforms that need a different policy.&lt;/p&gt;

&lt;p&gt;For Web, Unity 6.5 documentation directs developers toward &lt;code&gt;Compressed In Memory&lt;/code&gt; and &lt;code&gt;Decompress On Load&lt;/code&gt;; do not blindly copy the native &lt;code&gt;long BGM = Streaming&lt;/code&gt; rule. Browser and Unity behavior can change, so recheck the exact Unity version and supported browsers.&lt;/p&gt;

&lt;p&gt;Browsers also generally require a click, tap, or key input before playback can begin. Initialize or resume audio after a user gesture, and test output latency and background-tab recovery separately. Load Type alone cannot solve these constraints.&lt;/p&gt;

&lt;p&gt;Unity also documents a possible Web loop glitch caused by AAC encoding and a workaround involving at least 1,024 silent samples at the beginning of a WAV plus loop information in the &lt;code&gt;smpl&lt;/code&gt; chunk. Verify it on the browsers you support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Profile the built game
&lt;/h2&gt;

&lt;p&gt;Connect the Audio Profiler to a Player build and reproduce the maximum realistic overlap.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it helps explain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DSP CPU&lt;/td&gt;
&lt;td&gt;Mixer, effects, and Compressed In Memory decoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming CPU&lt;/td&gt;
&lt;td&gt;Streaming work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total Audio Memory&lt;/td&gt;
&lt;td&gt;Overall audio-engine allocation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sample Sound Memory&lt;/td&gt;
&lt;td&gt;Decoded sample residency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming File / Decode Memory&lt;/td&gt;
&lt;td&gt;Streaming buffers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playing Audio Sources / Audio Voices&lt;/td&gt;
&lt;td&gt;Playback and voice count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual / Plays&lt;/td&gt;
&lt;td&gt;Virtualization and unexpected repeats&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;more Decompress On Load   → watch Sample Sound Memory
more Compressed In Memory → watch DSP CPU
more Streaming            → watch Streaming CPU and memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Memory can retain a high-water mark for reuse, so it may not return to the startup value immediately. Compare clean startup, maximum combat, repeated scene transitions, large dialogue sessions, and long play sessions.&lt;/p&gt;

&lt;p&gt;Change one axis at a time. When comparing Load Types, keep format, quality, channels, sample rate, and simultaneous count fixed. Record the Unity version, platform, device, importer settings, clip count, total duration, CPU, memory, startup delay, underruns, loop problems, and audible differences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mistake&lt;/th&gt;
&lt;th&gt;Better interpretation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WAV means the shipped game is uncompressed&lt;/td&gt;
&lt;td&gt;Unity re-encodes for the target platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Everything should be PCM&lt;/td&gt;
&lt;td&gt;Long clips inflate build size and decoded memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADPCM makes runtime memory 3.5 times smaller&lt;/td&gt;
&lt;td&gt;Decompress On Load still needs decoded samples&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vorbis is always optimal&lt;/td&gt;
&lt;td&gt;Many simultaneous decoders can raise CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every short voice line should stream&lt;/td&gt;
&lt;td&gt;Per-stream overhead accumulates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turning Preload off fixes memory&lt;/td&gt;
&lt;td&gt;It can move loading to the first playback request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background loading means instant playback&lt;/td&gt;
&lt;td&gt;Playback can wait for completion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BGM, SFX, and Voice are enough categories&lt;/td&gt;
&lt;td&gt;Duration, concurrency, trigger timing, and platform matter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Production decision flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Keep an uncompressed or lossless source master.&lt;/li&gt;
&lt;li&gt;Classify clips by behavior: long/short BGM, UI/frequent/medium SFX, dialogue/bark/long voice.&lt;/li&gt;
&lt;li&gt;Use PCM for tiny timing-critical sounds, test ADPCM for repeated noisy SFX, and use Vorbis as the default candidate for medium and long content.&lt;/li&gt;
&lt;li&gt;Use Decompress On Load when the decoded total is small, Compressed In Memory when compressed residency is preferable, and Streaming for genuinely long predictable audio.&lt;/li&gt;
&lt;li&gt;Preload input-, hit-, physics-, and animation-driven sounds instead of streaming them.&lt;/li&gt;
&lt;li&gt;Review mono and sample-rate options by content, not folder name alone.&lt;/li&gt;
&lt;li&gt;Prefetch dialogue and music; align synchronized assets and use &lt;code&gt;PlayScheduled()&lt;/code&gt; for DSP-timeline starts.&lt;/li&gt;
&lt;li&gt;Measure and listen on the minimum-spec device at maximum realistic load.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The runtime quality and cost of Unity audio are not decided when a WAV enters the Assets folder. Platform conversion, Compression Format, quality, sample rate, channel count, Load Type, preload policy, distribution pipeline, and concurrency determine the shipped result.&lt;/p&gt;

&lt;p&gt;A reasonable first pass is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;long BGM: &lt;code&gt;Vorbis + Streaming&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;UI and action feedback: &lt;code&gt;PCM + Decompress On Load + preloading&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;footsteps, impacts, and weapons: test &lt;code&gt;ADPCM&lt;/code&gt; and preload event-driven clips;&lt;/li&gt;
&lt;li&gt;medium SFX: &lt;code&gt;Vorbis + Compressed In Memory&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;dialogue: &lt;code&gt;Mono + Vorbis + Compressed In Memory&lt;/code&gt; with prefetching;&lt;/li&gt;
&lt;li&gt;long narration: &lt;code&gt;Mono + Vorbis + Streaming&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;battle barks: &lt;code&gt;PCM/ADPCM + Decompress On Load&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final choice belongs to the real clip duration, loaded and playing counts, trigger timing, target device, profiler data, and listening result.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/class-AudioClip.html" rel="noopener noreferrer"&gt;Audio Clip Import Settings — Unity 6.5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.2/Documentation/Manual/AudioFiles-compression.html" rel="noopener noreferrer"&gt;Audio file compression — Unity 6.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/ProfilerAudio.html" rel="noopener noreferrer"&gt;Audio Profiler — Unity 6.5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/webgl-audio.html" rel="noopener noreferrer"&gt;Audio on Web — Unity 6.5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/AudioClip.GetData.html" rel="noopener noreferrer"&gt;AudioClip.GetData — Unity 6.5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/AudioSource.PlayScheduled.html" rel="noopener noreferrer"&gt;AudioSource.PlayScheduled — Unity 6.5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.addressables@3.0/manual/AddressableAssetsAsyncOperationHandle.html" rel="noopener noreferrer"&gt;Addressables operation handles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.addressables@3.0/manual/ContentPackingAndLoadingSchema.html" rel="noopener noreferrer"&gt;Addressables Group settings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/AssetPostprocessor.OnPreprocessAudio.html" rel="noopener noreferrer"&gt;OnPreprocessAudio — Unity 6.5&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>performance</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Where Unity Audio Reaches Its Limits: CRI ADX vs Wwise vs FMOD Studio</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Wed, 12 Aug 2026 03:54:52 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/where-unity-audio-reaches-its-limits-cri-adx-vs-wwise-vs-fmod-studio-1m4f</link>
      <guid>https://dev.to/gamedevtoollab/where-unity-audio-reaches-its-limits-cri-adx-vs-wwise-vs-fmod-studio-1m4f</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Unity can play, mix, stream, spatialize, schedule, and profile audio without third-party middleware. Many small and medium projects can ship with &lt;code&gt;AudioSource&lt;/code&gt;, Audio Mixer, Audio Random Container, and modest supporting code.&lt;/p&gt;

&lt;p&gt;Larger productions still adopt CRI ADX, Wwise, or FMOD Studio. The reason is usually not lower sound quality or automatically better performance. The practical boundary is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can the game keep treating audio as clips triggered by code, or must audio behavior become independently authored data that sound designers can edit, package, profile, and validate?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Beyond that boundary, Unity can reproduce many features, but the team must maintain event definitions, parameter mapping, voice policy, content delivery, live tuning, validation, and diagnostics.&lt;/p&gt;

&lt;p&gt;This article compares Unity 6.5 built-in audio, CRI ADX including ADX LE, Wwise 2025.1.9, and FMOD Studio 2.03. The fixed Wwise 2025.1.9 Public Library is used because Wwise 2026.1 was still beta on the research date. Version and licensing notes are based on official information checked on July 29, 2026; recheck current terms before release.&lt;/p&gt;

&lt;h2&gt;
  
  
  The conclusion first: operational complexity matters more than sound count
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;th&gt;Main reason to choose it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unity built-in audio&lt;/td&gt;
&lt;td&gt;Small to medium, comparatively simple, programmer-led projects&lt;/td&gt;
&lt;td&gt;Keep dependencies and tooling small&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CRI ADX&lt;/td&gt;
&lt;td&gt;Mobile or console titles with many voices and streams&lt;/td&gt;
&lt;td&gt;Manage cues, compression, voice control, and delivery together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wwise&lt;/td&gt;
&lt;td&gt;Large projects with complex states, music, profiling, and several audio contributors&lt;/td&gt;
&lt;td&gt;Formalize a scalable audio pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FMOD Studio&lt;/td&gt;
&lt;td&gt;Projects that value DAW-like authoring and fast device iteration&lt;/td&gt;
&lt;td&gt;Build parameter-driven events quickly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Middleware deserves evaluation when several of these are firm requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sound designers must change layers, randomization, transitions, and mix behavior without a programmer.&lt;/li&gt;
&lt;li&gt;One playback request represents an event rather than one clip.&lt;/li&gt;
&lt;li&gt;Continuous game values such as RPM, health, or combat intensity affect sound.&lt;/li&gt;
&lt;li&gt;Voice limits need category-specific survival rules.&lt;/li&gt;
&lt;li&gt;Voice data must be delivered by language, chapter, event, or DLC.&lt;/li&gt;
&lt;li&gt;Sound designers must tune and profile a running device build.&lt;/li&gt;
&lt;li&gt;Codec, memory, and streaming policy must stay consistent across platforms.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As a screening rule rather than an official threshold, three or more clear requirements justify a vertical-slice comparison. One or two may still be simpler with a thin layer over Unity Audio.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Unity's built-in audio already covers
&lt;/h2&gt;

&lt;p&gt;Unity 6.5 already provides 2D/3D playback, attenuation, priority, Audio Mixer routing and effects, snapshots, exposed parameters, Audio Random Container, and scheduled playback. It also provides three load types, platform-specific compression, real and virtual voice limits, the Audio Profiler, spatializer plug-ins, and the Scriptable Audio Pipeline.&lt;/p&gt;

&lt;p&gt;A random footstep, pause snapshot, streamed BGM, or voice virtualization does not by itself require middleware. Audio Random Container handles simple variation, but compound conditions still need a separate data model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use the Audio Mixer first
&lt;/h3&gt;

&lt;p&gt;At minimum, separate major categories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Master
├─ Music
├─ SFX
│  ├─ Player
│  ├─ Enemy
│  ├─ Environment
│  └─ UI
└─ Voice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This supports user volume, pause states, ducking, and category-specific processing. Once a Mixer parameter is set from script, snapshots no longer control that value until the override is cleared. Give exposed parameters explicit ownership: do not stack user volume, cinematic modifiers, pause or mute state, and accessibility adjustments onto one ambiguous value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choose load type by use case
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decompress On Load:&lt;/strong&gt; short, frequent effects; higher memory, lower repeated decode work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compressed In Memory:&lt;/strong&gt; lower memory, more playback-time decode work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming:&lt;/strong&gt; long music and ambience&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unity documents useful scale estimates: decompressed Vorbis may use roughly ten times its compressed size, ADPCM roughly 3.5 times, and each streaming clip has about 200 KB of overhead. Decide from duration, concurrency, latency, memory, and target-device performance.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PlayScheduled&lt;/code&gt; can implement bar-aligned transitions and synchronized stems. The growing cost is ownership of BPM data, transition points, cancellation, preview, and debugging. Likewise, the Scriptable Audio Pipeline enables custom processing but does not provide event authoring, bank generation, localization packaging, live tuning, or a dedicated sound-design profiler; it also does not support Web platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Unity Audio reaches its practical limit
&lt;/h2&gt;

&lt;p&gt;There is no universal clip or voice count where Unity stops working. The practical limit is reached when maintaining the custom workflow costs more than integrating middleware.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. A playback request becomes an event
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_audioSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PlayOneShot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hitClip&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 enough until one hit requires non-repeating variations, weapon and surface selection, near and far layers, a critical layer, random pitch, an instance cap, and player-over-enemy priority.&lt;/p&gt;

&lt;p&gt;The game now wants to post &lt;code&gt;PlayerAttackHit&lt;/code&gt; with values such as &lt;code&gt;Material=Metal&lt;/code&gt; and &lt;code&gt;Critical=true&lt;/code&gt;. Middleware keeps selection, layers, randomization, voice limits, and mix behavior in authored data. Unity can do the same with ScriptableObjects and custom inspectors, but the team is then building a small middleware layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Every adjustment requires a programmer
&lt;/h3&gt;

&lt;p&gt;If changing footstep variation or a music transition requires C#, Prefabs, Timeline, or scene-level sources, every adjustment becomes a designer-to-programmer handoff followed by another build and review. A few minutes saved once is trivial; the same loop repeated hundreds of times is not. Dedicated authoring and live connection move routine iteration to the sound designer.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Several states and values affect one sound
&lt;/h3&gt;

&lt;p&gt;Vehicles may combine RPM, throttle, speed, surface, camera perspective, and damage. Action games combine combat intensity, enemy count, health, area, and weather. Custom implementations tend to scatter mappings, curves, and exceptions across scripts.&lt;/p&gt;

&lt;p&gt;Wwise States, Switches, and RTPCs; FMOD parameters; and CRI ADX AISACs and selectors make the mapping from game state to audio behavior explicit and editable.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The hard problem is which voices survive
&lt;/h3&gt;

&lt;p&gt;Unity has real and virtual voice limits plus &lt;code&gt;AudioSource.priority&lt;/code&gt;. Middleware does not remove hardware limits. The difference appears when policy becomes specific: UI must survive, player voice outranks enemy chatter, one enemy may have one growl, distant footsteps virtualize, and over-limit events steal the oldest, quietest, or farthest instance. Event-, bus-, cue-, or pool-level rules are easier to author and inspect than one global priority value.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Audio becomes delivered content
&lt;/h3&gt;

&lt;p&gt;Live games may split voices by language, chapter, or event; stream music; delete completed story data; and select codecs per platform. Unity plus Addressables can do this, but the team must connect groups to event definitions, languages, references, and lifetime.&lt;/p&gt;

&lt;p&gt;CRI ACB/AWB files, Wwise SoundBanks, and FMOD Banks connect authoring to delivery units. Addressables does not make middleware lifetime automatic. For example, FMOD Banks still require explicit loading and unloading, required Master and Strings Banks, and awareness that releasing an AssetBundle does not unload a Bank.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. You must explain why a sound is missing
&lt;/h3&gt;

&lt;p&gt;Unity's Audio Profiler is useful for voice count, CPU, memory, and audibility. Complex projects may also need the posting object, selected parameter branch, virtualization or stealing reason, loaded localized waveform, Bank memory, and the effect of a live mix edit. Dedicated audio profiling lets the sound team investigate this directly instead of requesting new programmer logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  CRI ADX: resource control and a workflow aligned with Japanese game production
&lt;/h2&gt;

&lt;p&gt;CRI ADX uses CRI Atom Craft for authoring and generally exposes cues to the game. Its production toolkit includes AISACs for continuous control, selectors for discrete states, REACT for category interactions, ACB/AWB delivery units, HCA/HCA-MX codecs, voice pools, profiling, in-game preview, and Atom Craft Robot automation.&lt;/p&gt;

&lt;p&gt;It is a strong candidate for mobile, console, live-service, and voice-heavy projects where compression, streaming, voice limits, and delivery must be designed together. Paid ADX plans include support; ADX LE does not include individual support.&lt;/p&gt;

&lt;p&gt;Treat the Atom Craft project, generated files, Unity plug-in, and Cue Sheet lifetime as one pipeline. Define naming, generation ownership, and preview integration early.&lt;/p&gt;

&lt;p&gt;As of July 29, 2026, ADX LE distribution required previous-year revenue and cumulative content revenue to remain at or below JPY 10 million. It also required self-publishing plus specified copyright and end-user license notices. The logo was recommended rather than mandatory. Projects outside those conditions, including publisher-distributed titles, need a paid agreement.&lt;/p&gt;

&lt;p&gt;CRI's FAQ also states that paid ADX and ADX LE output data are not mutually compatible. A commercial project likely to exceed LE should discuss an evaluation license early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wwise: formalizing complex systems and scaling large pipelines
&lt;/h2&gt;

&lt;p&gt;Wwise combines event authoring, game states, interactive music, spatial audio, SoundBanks, profiling, and automation.&lt;/p&gt;

&lt;p&gt;Its State, Switch, and RTPC distinction is particularly useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;State:&lt;/strong&gt; a broad condition such as paused, underwater, or low health&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch:&lt;/strong&gt; an object-specific selection such as dirt, stone, or metal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTPC:&lt;/strong&gt; a continuous value such as RPM, health, or combat intensity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Music Segments, containers, transition rules, and stingers support beat-aware behavior. The authoring tool can connect to builds to inspect game objects, voices, memory, streaming, and SoundBanks. WAAPI enables bulk registration, object creation, and validation from external tools.&lt;/p&gt;

&lt;p&gt;Wwise fits projects with several audio contributors, many states, complex music or spatial audio, deep runtime analysis, and automation. The trade-off is organizational weight. Define Game Object lifetime, naming, SoundBank split and lifetime, Work Unit source control, build configuration, and the Wwise/Unity Integration update procedure at the start.&lt;/p&gt;

&lt;p&gt;Licensing varies across non-commercial, limited-commercial, commercial core, support, and optional features. Confirm budget, platforms, plug-ins, and support before production costs are locked.&lt;/p&gt;

&lt;h2&gt;
  
  
  FMOD Studio: authoring speed with an approachable structure
&lt;/h2&gt;

&lt;p&gt;FMOD Studio builds events through a timeline and parameter sheets, then packages them into Banks. Its DAW-like presentation often gives composers and sound designers a direct route into implementation.&lt;/p&gt;

&lt;p&gt;Parameters can be Continuous, Discrete, or Labeled; built-in values include distance, direction, and elevation. Events can switch and layer sources while automating gain, pitch, filters, and effects. Live Update connects Studio to a running game for immediate inspection and tuning.&lt;/p&gt;

&lt;p&gt;Transition Markers and Regions, Magnet Regions, Loop Regions, and Tempo Markers support conditional, beat-aware music. Instance limits can steal the oldest, quietest, or farthest instance, or virtualize it.&lt;/p&gt;

&lt;p&gt;FMOD fits teams that value fast parameter and music iteration. Establish naming for events, Banks, parameters, buses, and snapshots before content grows, and keep Studio and Unity Integration versions aligned. FMOD generally recommends disabling Unity's built-in audio system, so staged migration needs a defined coexistence period.&lt;/p&gt;

&lt;p&gt;As of July 29, 2026, game licensing listed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Indie:&lt;/strong&gt; below a USD 600,000 development budget, free or USD 2,000&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Basic:&lt;/strong&gt; USD 600,000 to USD 1.8 million, USD 6,000&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Premium:&lt;/strong&gt; above USD 1.8 million, USD 18,000&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Free Indie License also required annual revenue below USD 200,000. Commercial projects require registration, and plan conditions include logo requirements. Verify the current EULA and budget definitions before contracting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production-oriented comparison
&lt;/h2&gt;

&lt;p&gt;This is a practical assessment, not an official ranking. Team experience, outsourcing partners, genre, and existing pipeline can reverse several rows.&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;Unity built-in&lt;/th&gt;
&lt;th&gt;CRI ADX&lt;/th&gt;
&lt;th&gt;Wwise&lt;/th&gt;
&lt;th&gt;FMOD Studio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Low initial integration cost&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Fair&lt;/td&gt;
&lt;td&gt;Fair&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Work entirely inside Unity&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Fair&lt;/td&gt;
&lt;td&gt;Fair&lt;/td&gt;
&lt;td&gt;Fair&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Independent sound-designer iteration&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Randomization, layers, and branching&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continuous parameter-driven audio&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complex interactive music&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Good–Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice limiting and virtualization&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compression, streaming, and delivery units&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live tuning and dedicated profiling&lt;/td&gt;
&lt;td&gt;Limited–Good&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large-scale automation&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Good–Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Good–Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low learning and operating cost&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Japanese-language onboarding&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Limited–Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Predictable external license cost&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not count “Excellent” cells. In a small project, breadth becomes training, build, and version-management cost. Unity's smaller feature set is valuable when it keeps ownership simple. Support experience also varies by region, reseller, contract, outsourcing company, and internal expertise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare lifetime ownership, not free versus paid
&lt;/h2&gt;

&lt;p&gt;A custom Unity Audio layer may begin at a few hundred lines, but its real cost includes custom editors, Unity-version maintenance, validation, voice policy, Addressables lifetime, profiling views, documentation, staff turnover, and platform investigation.&lt;/p&gt;

&lt;p&gt;Middleware adds SDK and native plug-in updates, tool training, generated-content management, platform integration, licenses, and compatibility among runtime, authoring tool, Unity integration, and Banks.&lt;/p&gt;

&lt;p&gt;In either case, separate editable sources from generated output, assign loading ownership, pin compatible versions, define rollback, remove profiling connections from release builds, and divide programmer, sound designer, and build-owner responsibilities. Compare total cost across the title's lifetime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing by project type
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small premium 2D game:&lt;/strong&gt; start with Unity Audio and a small logical-event layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice-heavy mobile live service:&lt;/strong&gt; evaluate CRI ADX and design Cue Sheets by language, chapter, and event early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mid-sized PC or console title with dedicated audio staff:&lt;/strong&gt; FMOD Studio is a practical vertical-slice candidate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large action RPG with complex music and spatial audio:&lt;/strong&gt; evaluate Wwise and assign a technical audio owner.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For contract work, the client's or publisher's existing standard often matters more than a feature-table advantage. Naming, content split, contracts, handoff procedure, and internal expertise are production assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Abstract audio before &lt;code&gt;AudioSource&lt;/code&gt; calls spread everywhere
&lt;/h2&gt;

&lt;p&gt;If middleware may be introduced later, avoid calling &lt;code&gt;AudioSource&lt;/code&gt; or product-specific APIs throughout 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;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;readonly&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;AudioEventId&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;Value&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="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;AudioEventId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Value&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;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;AudioHandle&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;Value&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="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;AudioHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Value&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;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IGameAudio&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;AudioHandle&lt;/span&gt; &lt;span class="nf"&gt;Play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;AudioEventId&lt;/span&gt; &lt;span class="n"&gt;eventId&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="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;emitter&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;void&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;AudioHandle&lt;/span&gt; &lt;span class="n"&gt;handle&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;fadeSeconds&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;void&lt;/span&gt; &lt;span class="nf"&gt;SetParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AudioHandle&lt;/span&gt; &lt;span class="n"&gt;handle&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;float&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetGlobalParameter&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;float&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="k"&gt;group&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;state&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 boundary example. Production code also needs asynchronous Bank or Cue Sheet loading, failures, cancellation, handle validity, playback completion, emitter lifetime, and unloading during scene transitions.&lt;/p&gt;

&lt;p&gt;Gameplay posts a logical event such as &lt;code&gt;SFX/Player/SwordHit&lt;/code&gt;. A Unity implementation maps it to a container or clip; ADX to a cue; Wwise to an Event; FMOD to an Event Reference.&lt;/p&gt;

&lt;p&gt;Centralize unregistered-event detection, playback-before-load errors, emitter lifetime, 3D position policy, content state, parameter validation, and diagnostics. Prefer generated code, typed IDs, GUID references, or validated assets over raw strings spread across gameplay code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run a three-day vertical-slice evaluation
&lt;/h2&gt;

&lt;p&gt;First eliminate candidates that fail platform, contract, distribution, team-experience, outsourcing, or CI requirements. Then implement the same small scene with the two strongest options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Footsteps across surfaces and speeds.&lt;/li&gt;
&lt;li&gt;Exploration, alert, and combat music transitions.&lt;/li&gt;
&lt;li&gt;Voice-driven ducking.&lt;/li&gt;
&lt;li&gt;One hundred to two hundred simultaneous requests with priority rules.&lt;/li&gt;
&lt;li&gt;3D ambience.&lt;/li&gt;
&lt;li&gt;Two voice languages.&lt;/li&gt;
&lt;li&gt;Bank or Cue Sheet load and unload.&lt;/li&gt;
&lt;li&gt;Live profiling plus CI generation or validation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Measure integration hours, independent sound-designer changes, edit-to-device steps, time to diagnose a missing sound, runtime visibility, Git clarity, new-machine setup, and removal of debug connections from release builds.&lt;/p&gt;

&lt;p&gt;Define scoring weights before seeing results, and test minimum-spec hardware with production-like data. Choose the product that removes your bottleneck, not the one with the longest feature list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common misconceptions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;“Unity Audio has lower sound quality.”&lt;/strong&gt; Quality depends on source material, compression, sample rate, mix, and output hardware. Unity's internal use of FMOD technology is not FMOD Studio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Middleware is always faster.”&lt;/strong&gt; It also has runtime, decode, DSP, and management costs. Its advantage is systematic control, which still requires target-hardware measurement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“The sound team no longer needs programmers.”&lt;/strong&gt; Events, objects, parameters, states, and content lifetime still cross the game-audio boundary. Middleware reduces routine blocking; it does not remove integration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“We can add it near the end.”&lt;/strong&gt; Late migration replaces scattered sources, Timeline tracks, clip references, Addressables groups, settings, and tests while introducing new event and delivery models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“A free license means zero cost.”&lt;/strong&gt; Training, integration, CI, updates, and incident response still consume engineering time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final recommendation
&lt;/h2&gt;

&lt;p&gt;Keep Unity's built-in audio when a small or medium project can meet its requirements without building a large custom workflow.&lt;/p&gt;

&lt;p&gt;The limit is not reached when Unity can no longer play a sound.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;It is reached when audio behavior is scattered through game code and the team must continuously maintain its own tools for authoring, delivery, voice policy, live iteration, and investigation.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Beyond that point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose &lt;strong&gt;CRI ADX&lt;/strong&gt; for voice-heavy, streaming-heavy, resource-conscious production and strong alignment with Japanese game development.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Wwise&lt;/strong&gt; for complex states, music, spatial audio, large teams, profiling depth, and automation.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;FMOD Studio&lt;/strong&gt; for fast timeline- and parameter-oriented iteration.&lt;/li&gt;
&lt;li&gt;Keep &lt;strong&gt;Unity Audio&lt;/strong&gt; when minimizing dependencies and keeping the system small matters most.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prioritize production experience, client or publisher standards, and the team's ability to investigate failures. A controlled vertical slice reveals more than counting check marks in a feature table.&lt;/p&gt;

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

&lt;p&gt;Specifications and licensing terms change. Check current documentation before adoption or release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unity
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/Audio.html" rel="noopener noreferrer"&gt;Unity 6.5 Manual: Audio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/AudioRandomContainer-UI.html" rel="noopener noreferrer"&gt;Audio Random Container reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/AudioMixerOverview.html" rel="noopener noreferrer"&gt;Introduction to the Audio Mixer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/class-AudioClip.html" rel="noopener noreferrer"&gt;Audio Clip Import Settings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/audio-scriptable-processors.html" rel="noopener noreferrer"&gt;Scriptable audio pipeline&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/Glossary.html#FMOD" rel="noopener noreferrer"&gt;Unity Glossary: FMOD&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CRI ADX
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://game.criware.jp/products/adx-le/" rel="noopener noreferrer"&gt;CRI ADX LE product and distribution conditions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://game.criware.jp/products/adx-le/le-faq/" rel="noopener noreferrer"&gt;CRI ADX LE FAQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://game.criware.jp/manual/native/adx2/latest/criatom_tools_atomcraft_about.html" rel="noopener noreferrer"&gt;About CRI Atom Craft&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://game.criware.jp/manual/native/adx2/latest/criatom_tools_atomcraft_preview_ingame.html" rel="noopener noreferrer"&gt;In-game preview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://game.criware.jp/manual/native/adx2/latest/criatom_tools_atomcraft_profiler.html" rel="noopener noreferrer"&gt;CRI Atom Profiler&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Wwise
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.audiokinetic.com/en/public-library/2025.1.9_9197/" rel="noopener noreferrer"&gt;Wwise 2025.1.9 Public Library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.audiokinetic.com/en/public-library/2025.1.9_9197/?id=concept_state.html&amp;amp;source=SDK" rel="noopener noreferrer"&gt;Understanding States — Wwise 2025.1.9&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.audiokinetic.com/en/public-library/2025.1.9_9197/?id=concept_switch.html&amp;amp;source=SDK" rel="noopener noreferrer"&gt;Understanding Switches — Wwise 2025.1.9&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.audiokinetic.com/en/public-library/2025.1.9_9197/?id=concept_rtpc.html&amp;amp;source=SDK" rel="noopener noreferrer"&gt;Understanding RTPCs — Wwise 2025.1.9&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.audiokinetic.com/en/community/blog/wwise-2026.1-whats-new/" rel="noopener noreferrer"&gt;Wwise 2026.1: What's New&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.audiokinetic.com/en/wwise/pricing/" rel="noopener noreferrer"&gt;Wwise Pricing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  FMOD Studio
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.fmod.com/licensing" rel="noopener noreferrer"&gt;FMOD Licensing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fmod.com/docs/2.03/studio/parameters.html" rel="noopener noreferrer"&gt;FMOD Studio 2.03: Parameters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fmod.com/docs/2.03/studio/authoring-events.html" rel="noopener noreferrer"&gt;FMOD Studio 2.03: Authoring Events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fmod.com/docs/2.03/unity/user-guide.html" rel="noopener noreferrer"&gt;FMOD for Unity 2.03 User Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>gameaudio</category>
      <category>tooling</category>
    </item>
    <item>
      <title>ASTC Isn't Automatically the Right Answer: Mobile Texture Formats for Unity on iOS and Android</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Mon, 10 Aug 2026 13:05:38 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/astc-isnt-automatically-the-right-answer-mobile-texture-formats-for-unity-on-ios-and-android-4m5g</link>
      <guid>https://dev.to/gamedevtoollab/astc-isnt-automatically-the-right-answer-mobile-texture-formats-for-unity-on-ios-and-android-4m5g</guid>
      <description>&lt;p&gt;When targeting modern iOS and Android devices in Unity, ASTC is often the first texture format people reach for. It supports RGB and RGBA, offers block sizes from 4x4 through 12x12, and gives you a practical way to trade image quality for memory and bandwidth.&lt;/p&gt;

&lt;p&gt;But "use ASTC" is not a complete production strategy.&lt;/p&gt;

&lt;p&gt;You still need answers to questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which iPhones and Android devices can actually use ASTC?&lt;/li&gt;
&lt;li&gt;Does Android support depend on the OS version, OpenGL ES, Vulkan, or the GPU?&lt;/li&gt;
&lt;li&gt;Is ASTC 6x6 unfriendly to GPUs because 6 is not a power of two?&lt;/li&gt;
&lt;li&gt;Is it reasonable to use 6x6 for every texture?&lt;/li&gt;
&lt;li&gt;What happens when Unity loads ASTC on unsupported hardware?&lt;/li&gt;
&lt;li&gt;How is a PNG or JPEG file size related to runtime texture memory?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article focuses on those production decisions rather than treating ASTC as simply "the newer mobile compression format." The terminology and Unity settings are based mainly on Unity 6.4 documentation as of July 2026. If a later Unity version changes the UI or delivery behavior, use the documentation for that version.&lt;/p&gt;

&lt;p&gt;The short version is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On Apple hardware, A8 is the hardware boundary for LDR ASTC, but that is &lt;strong&gt;not&lt;/strong&gt; the same thing as the minimum device you can ship to with a current Unity project.&lt;/li&gt;
&lt;li&gt;For Android phones and tablets distributed through Google Play, a robust default is an AAB with ETC2 as the default texture format and ASTC as an additional format through Texture Compression Targeting.&lt;/li&gt;
&lt;li&gt;ASTC 6x6 is not a problem just because 6 is not a power of two. It is a standard hardware texture format with 128-bit blocks.&lt;/li&gt;
&lt;li&gt;Do not force every texture to 6x6. A useful starting point is 6x6 for general albedo/sprites, 4x4-5x5 for text, SDFs, important normals and high-detail UI, and 8x8 for low-frequency backgrounds when visual testing allows it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only want the actionable settings, skip to &lt;strong&gt;A practical project policy&lt;/strong&gt; near the end. If you are specifically interested in why 6x6 works well on GPUs, read &lt;strong&gt;Is ASTC 6x6 a problem because it is not power-of-two?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  First, separate three different meanings of "image format"
&lt;/h2&gt;

&lt;p&gt;Texture discussions often mix three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Source files&lt;/strong&gt;: PNG, JPEG, PSD, TIFF, EXR. Their file size does not directly determine GPU memory. A 1024x1024 texture expanded to RGBA32 is about 4 MiB without mipmaps even if its PNG is tiny.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build/delivery containers&lt;/strong&gt;: APK, AAB, AssetBundle, Addressables bundles, LZ4, LZMA. These affect download size, storage, and loading, but do not change the GPU footprint of a texture once uploaded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU texture formats&lt;/strong&gt;: ASTC, ETC2, PVRTC, BCn, RGBA32. These are designed for random-access sampling by the texture hardware and are the focus of this article.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keeping the layers separate prevents mistakes such as "a smaller PNG means less VRAM" or "LZ4-compressing an AssetBundle reduces texture memory."&lt;/p&gt;

&lt;h2&gt;
  
  
  What ASTC changes
&lt;/h2&gt;

&lt;p&gt;ASTC (Adaptive Scalable Texture Compression) covers RGB/RGBA, linear/sRGB, multiple bit rates, LDR and supported HDR paths, and a range of square and non-square block footprints.&lt;/p&gt;

&lt;p&gt;The important point is that ASTC 4x4, 6x6, and 8x8 are &lt;strong&gt;not encoder quality presets&lt;/strong&gt;. They specify how many texels are represented by one 128-bit compressed block.&lt;/p&gt;

&lt;h2&gt;
  
  
  ASTC block size and bit rate
&lt;/h2&gt;

&lt;p&gt;Every 2D ASTC block is 128 bits, or 16 bytes. Increasing the number of texels represented by that block lowers the amount of information available per texel and therefore lowers the memory footprint.&lt;/p&gt;

&lt;p&gt;The theoretical bit rate is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bits per pixel = 128 / (block width * block height)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A useful subset of Unity's options 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;ASTC&lt;/th&gt;
&lt;th&gt;Texels per block&lt;/th&gt;
&lt;th&gt;Bit rate&lt;/th&gt;
&lt;th&gt;1024x1024, no mips&lt;/th&gt;
&lt;th&gt;Typical starting use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4x4&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;8.00 bpp&lt;/td&gt;
&lt;td&gt;1.00 MiB&lt;/td&gt;
&lt;td&gt;High-quality UI, text, visible detail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5x5&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;5.12 bpp&lt;/td&gt;
&lt;td&gt;~657 KiB&lt;/td&gt;
&lt;td&gt;High-quality general textures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6x6&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;3.56 bpp&lt;/td&gt;
&lt;td&gt;~457 KiB&lt;/td&gt;
&lt;td&gt;General quality/memory balance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8x8&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;2.00 bpp&lt;/td&gt;
&lt;td&gt;256 KiB&lt;/td&gt;
&lt;td&gt;Backgrounds and low-frequency imagery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10x10&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1.28 bpp&lt;/td&gt;
&lt;td&gt;~166 KiB&lt;/td&gt;
&lt;td&gt;Aggressive size reduction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12x12&lt;/td&gt;
&lt;td&gt;144&lt;/td&gt;
&lt;td&gt;0.89 bpp&lt;/td&gt;
&lt;td&gt;~116 KiB&lt;/td&gt;
&lt;td&gt;Very aggressive, specialized use&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Because dimensions such as 1024 are not divisible by every ASTC block size, the real base-level size uses rounded-up block counts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ceil(width / blockWidth)
* ceil(height / blockHeight)
* 16 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a sufficiently large 2D texture, a complete mip chain is often roughly 4/3 the base level. Small mip levels introduce extra rounding because every level still needs at least one compression block.&lt;/p&gt;

&lt;p&gt;In this article, &lt;code&gt;MipMap&lt;/code&gt; refers to the Unity setting, &lt;code&gt;Mip&lt;/code&gt; to an individual level, and "mip chain" to the whole sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is ASTC 6x6 a problem because it is not power-of-two?
&lt;/h2&gt;

&lt;p&gt;No. &lt;strong&gt;The fact that 6 is not a power of two is not, by itself, a disadvantage.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ASTC deliberately defines many non-power-of-two footprints: 5x4, 5x5, 6x5, 6x6, 8x5, 8x6, 10x5, 10x6, 10x8, 12x10, and others.&lt;/p&gt;

&lt;p&gt;The old advice that "GPUs prefer powers of two" contains historical truth, but it refers to a different level of the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Texture dimensions and compression block dimensions are different things
&lt;/h3&gt;

&lt;p&gt;Older GPUs and APIs often restricted the width and height of the &lt;strong&gt;whole texture&lt;/strong&gt; to power-of-two values such as 256, 512, or 1024. NPOT textures sometimes had restrictions around mipmaps or repeat modes.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;6&lt;/code&gt; in ASTC 6x6 is not the texture width. It is the texel footprint represented by one compressed block.&lt;/p&gt;

&lt;p&gt;If a GPU exposes ASTC 6x6 support, its texture hardware is designed to address and decode that footprint. You do not need to avoid it because you imagine a general-purpose integer divide-by-six happening in shader code.&lt;/p&gt;

&lt;p&gt;Conceptually, a texel at &lt;code&gt;(x, y)&lt;/code&gt; belongs to a block like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blockX = floor(x / 6)
blockY = floor(y / 6)
localX = x mod 6
localY = y mod 6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That pseudocode describes the mapping, not the internal circuit implementation. The key layout property is that the compressed block itself is always 16 bytes. Once the block index is known, the storage address is a fixed-stride calculation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blockAddress = baseAddress
             + (blockY * blocksPerRow + blockX) * 16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So ASTC 6x6 is &lt;strong&gt;not&lt;/strong&gt; a raw 6x6 pixel array with an awkward six-byte alignment. Thirty-six texels are encoded into a 16-byte block. In memory, the compressed data is still a grid of fixed-size blocks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does 4x4 have exactly the same performance as 6x6?
&lt;/h3&gt;

&lt;p&gt;That is a different question, and the answer is not guaranteed to be yes.&lt;/p&gt;

&lt;p&gt;Different GPU generations can have different texture-unit throughput, cache behavior, anisotropic filtering costs, and decoder implementations. You should not assume every ASTC footprint has identical throughput on every GPU.&lt;/p&gt;

&lt;p&gt;But in production, the more important trade-off is usually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;6x6 uses substantially less texture memory than 4x4.&lt;/li&gt;
&lt;li&gt;Less compressed data can reduce external memory bandwidth pressure.&lt;/li&gt;
&lt;li&gt;The same cache capacity can cover more texels.&lt;/li&gt;
&lt;li&gt;Image quality is lower because the same 128 bits describe more texels.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So choose between 4x4 and 6x6 based on &lt;strong&gt;quality, memory, bandwidth, and measurements&lt;/strong&gt;, not because one block width happens to be a power of two.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a device support ASTC 4x4 but not 6x6?
&lt;/h3&gt;

&lt;p&gt;That is not the normal way ASTC LDR capability is modeled. For example, when Vulkan's &lt;code&gt;textureCompressionASTC_LDR&lt;/code&gt; feature is enabled, the standard LDR ASTC block formats are part of the supported feature set; 6x6 is not a separate optional extension.&lt;/p&gt;

&lt;p&gt;Unity still lets you query specific formats such as &lt;code&gt;TextureFormat.ASTC_6x6&lt;/code&gt;, which is useful for diagnostics, custom compressed data, and driver investigation. Think of the hardware capability as ASTC LDR support, while Unity's diagnostic API can inspect individual formats.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about texture edges and NPOT sizes?
&lt;/h2&gt;

&lt;p&gt;A 1024x1024 texture does not divide evenly into 6x6 blocks. It needs 171 blocks in each dimension, with the edge blocks covering a partially valid area.&lt;/p&gt;

&lt;p&gt;That is expected behavior. Block compression rounds block counts up.&lt;/p&gt;

&lt;p&gt;The overhead matters more for tiny textures. A 7x7 texture compressed as ASTC 6x6 needs 2x2 blocks, or 64 bytes. Its effective bit rate is about 10.45 bpp, far above the 3.56 bpp headline number for large images.&lt;/p&gt;

&lt;p&gt;For many tiny textures, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sprite Atlas or Texture Atlas packing&lt;/li&gt;
&lt;li&gt;R8, RG16, or even RGBA32 for very small data where appropriate&lt;/li&gt;
&lt;li&gt;grouping by loading and batching behavior, not just per-texture format&lt;/li&gt;
&lt;li&gt;disabling mipmaps for UI that does not need them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ASTC supporting NPOT textures does not mean every tiny texture is storage-efficient on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  ASTC support on iOS
&lt;/h2&gt;

&lt;p&gt;Apple hardware is easier to reason about than the Android ecosystem. The hardware boundary for LDR ASTC is the A8 generation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A8 is the hardware boundary for LDR ASTC. It is not the minimum shippable device list for a Unity 6.4 project.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unity 6.4's Player requirements specify iOS/iPadOS 15 or newer and A8 SoC or newer, and the project must also satisfy its Deployment Target, Xcode version, SDK dependencies, and current App Store requirements.&lt;/p&gt;

&lt;p&gt;A rough hardware view is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Apple GPU generation&lt;/th&gt;
&lt;th&gt;Example SoC&lt;/th&gt;
&lt;th&gt;LDR ASTC&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Apple1&lt;/td&gt;
&lt;td&gt;A7&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Early Metal generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple2+&lt;/td&gt;
&lt;td&gt;A8+&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;LDR ASTC available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple6+&lt;/td&gt;
&lt;td&gt;A13+&lt;/td&gt;
&lt;td&gt;HDR ASTC&lt;/td&gt;
&lt;td&gt;Treat separately from LDR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The iPhone 6 and 6 Plus used A8, while the iPhone 5s used A7. Those are &lt;strong&gt;SoC-generation examples&lt;/strong&gt;, not current Unity 6.4 deployment examples. The iPhone 6 family cannot satisfy an iOS 15+ requirement.&lt;/p&gt;

&lt;p&gt;The practical rule is to separate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;hardware capability&lt;/strong&gt; — whether the SoC can sample ASTC, and&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shipping eligibility&lt;/strong&gt; — whether the OS, Unity, Xcode, SDKs, and App Store rules still allow that device into your supported set.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If every device you actually ship to supports LDR ASTC, using ASTC as the standard iOS texture format is a natural choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  ASTC support on Android
&lt;/h2&gt;

&lt;p&gt;Android cannot be summarized as "Android version X or newer supports ASTC." The actual capability depends primarily on the GPU and driver.&lt;/p&gt;

&lt;p&gt;The Android Developers documentation gives rough Google Play ecosystem support figures of more than 80% for ASTC and more than 95% for ETC2. Those numbers do not predict your own audience, but they illustrate the important point: ASTC is mainstream, yet an ASTC-only Android strategy can still exclude a meaningful device segment.&lt;/p&gt;

&lt;p&gt;Unity 6.4 documentation lists examples of GPU generations commonly associated with ASTC support, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Qualcomm Adreno 4xx and later / Snapdragon 415 and later&lt;/li&gt;
&lt;li&gt;Arm Mali T624 and later&lt;/li&gt;
&lt;li&gt;NVIDIA Tegra K1 and later&lt;/li&gt;
&lt;li&gt;PowerVR GX6250 and later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat those as useful guidance, not a substitute for the actual API and driver capability on the device.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenGL ES 3.0 does not imply ASTC
&lt;/h3&gt;

&lt;p&gt;ETC2 is a standard requirement in OpenGL ES 3.0. ASTC LDR under OpenGL ES 3.0/3.1 is typically exposed through extensions such as &lt;code&gt;GL_KHR_texture_compression_astc_ldr&lt;/code&gt;, so the API version alone is insufficient.&lt;/p&gt;

&lt;p&gt;OpenGL ES 3.2 is different: the specification includes the 2D LDR ASTC formats in its required compressed texture formats. That still does not imply HDR ASTC, and it does not tell you what Unity imported, which graphics API Unity actually selected, or which asset slice Google Play delivered.&lt;/p&gt;

&lt;p&gt;A safer summary is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenGL ES 3.0 -&amp;gt; ETC2 can be expected.&lt;/li&gt;
&lt;li&gt;OpenGL ES 3.0 -&amp;gt; ASTC is not guaranteed.&lt;/li&gt;
&lt;li&gt;OpenGL ES 3.1 -&amp;gt; many modern devices support ASTC, but do not infer it from the version alone.&lt;/li&gt;
&lt;li&gt;OpenGL ES 3.2 -&amp;gt; 2D LDR ASTC is required by the specification.&lt;/li&gt;
&lt;li&gt;In Unity -&amp;gt; verify the active graphics environment and the relevant format support on real devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Vulkan support does not imply ASTC either
&lt;/h3&gt;

&lt;p&gt;Vulkan exposes a &lt;code&gt;textureCompressionASTC_LDR&lt;/code&gt; feature. A device being able to run Vulkan does not mean that feature must be enabled.&lt;/p&gt;

&lt;p&gt;The correct mental model is "Vulkan device plus ASTC feature," not "Vulkan equals ASTC."&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when Unity hits unsupported ASTC?
&lt;/h2&gt;

&lt;p&gt;Unity can fall back by decompressing an unsupported texture into an uncompressed format. Unity 6.4's Android documentation describes ASTC falling back to RGBA32 on unsupported hardware.&lt;/p&gt;

&lt;p&gt;That can keep the app running, but it is not free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU work during loading&lt;/li&gt;
&lt;li&gt;longer first-use or scene-transition stalls&lt;/li&gt;
&lt;li&gt;transient memory pressure&lt;/li&gt;
&lt;li&gt;much higher texture memory usage&lt;/li&gt;
&lt;li&gt;higher memory bandwidth&lt;/li&gt;
&lt;li&gt;increased risk of out-of-memory termination on constrained devices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ASTC 6x6 is about 3.56 bpp. RGBA32 is 32 bpp: roughly nine times as much data.&lt;/p&gt;

&lt;p&gt;For a 2048x2048 texture with no mipmaps, ASTC 6x6 is roughly 1.78 MiB after block rounding, while RGBA32 is 16 MiB. With a full mip chain, you are roughly around 2.4 MiB versus 21.3 MiB.&lt;/p&gt;

&lt;p&gt;One texture may be survivable. Dozens of characters, backgrounds, UI atlases, and effects can turn that fallback into a serious memory problem.&lt;/p&gt;

&lt;p&gt;Treat runtime fallback as a &lt;strong&gt;last safety net&lt;/strong&gt;, not a normal production path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Texture Compression Targeting on Google Play
&lt;/h2&gt;

&lt;p&gt;This section is about Android phones and tablets. If you also target Google Play Games for PC from the same delivery setup, review S3TC requirements separately instead of assuming ASTC + ETC2 covers that environment.&lt;/p&gt;

&lt;p&gt;With an Android App Bundle, Google Play can deliver different texture compression variants to different devices. In Unity 6.4, you configure multiple entries in &lt;code&gt;Texture Compression Formats&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A robust order for broad mobile distribution is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. ETC2  // default
2. ASTC  // additional optimized format
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why put ETC2 first if you prefer ASTC? The first entry is the &lt;strong&gt;default fallback&lt;/strong&gt;, not the universally preferred delivery format. Google Play can still deliver the ASTC variant to ASTC-capable devices while ETC2 covers the safer fallback path. Each device downloads one matching variant, reducing the need for RGBA32 runtime expansion.&lt;/p&gt;

&lt;p&gt;ETC2 is not visually equivalent to ASTC 6x6, so QA the ETC2 slice independently for alpha-heavy sprites, gradients, normals, and packed masks. Multiple variants also increase import/build time and CI storage, and per-texture Platform Overrides should be audited because they can defeat the global policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remote Addressables and custom AssetBundles are a separate problem
&lt;/h3&gt;

&lt;p&gt;Google Play's Texture Compression Targeting does not automatically rewrite arbitrary bundles hosted on your own CDN.&lt;/p&gt;

&lt;p&gt;If you maintain ASTC and ETC2 variants for remote Addressables or custom AssetBundles, treat them as separate content lines. Include the format in identifiers such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Addressables Profile&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RemoteLoadPath&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;catalog&lt;/li&gt;
&lt;li&gt;bundle name&lt;/li&gt;
&lt;li&gt;URL&lt;/li&gt;
&lt;li&gt;cache key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not publish different binary contents behind the same URL and hope the cache sorts it out. Keep Content Update flows format-specific as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Direct APK distribution
&lt;/h3&gt;

&lt;p&gt;Texture Compression Targeting assumes AAB delivery. Without AAB, Unity 6.4 uses &lt;strong&gt;only the first entry&lt;/strong&gt; in &lt;code&gt;Texture Compression Formats&lt;/code&gt; for the APK. If ETC2 is first and ASTC second, the APK is ETC2; an ASTC APK needs ASTC first in a separate build.&lt;/p&gt;

&lt;p&gt;For enterprise distribution, alternative stores, MDM, or testing APKs, choose a common ETC2 build, separate ASTC/ETC2 APKs, format-specific bundles, or an explicitly ASTC-only hardware requirement. ASTC-only APKs are reasonable in controlled device fleets but risky for broad consumer distribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is ASTC 6x6 a good default?
&lt;/h2&gt;

&lt;p&gt;Often, yes—as a &lt;strong&gt;starting point&lt;/strong&gt;, not as a rule.&lt;/p&gt;

&lt;p&gt;ASTC 6x6 is about 3.56 bpp, roughly 44% of ASTC 4x4's bit rate and around one ninth of RGBA32. That makes it a practical baseline for many game textures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consider 4x4 or 5x5 for
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;small text embedded in UI&lt;/li&gt;
&lt;li&gt;thin-line icons&lt;/li&gt;
&lt;li&gt;high-contrast edges&lt;/li&gt;
&lt;li&gt;faces, eyes, and other highly scrutinized character areas&lt;/li&gt;
&lt;li&gt;complex gradients mixed with fine detail&lt;/li&gt;
&lt;li&gt;important normal maps&lt;/li&gt;
&lt;li&gt;packed masks whose numeric precision matters&lt;/li&gt;
&lt;li&gt;SDF/font atlases&lt;/li&gt;
&lt;li&gt;low-resolution textures shown very large on screen&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6x6 is often a good starting point for
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;general character and environment albedo&lt;/li&gt;
&lt;li&gt;typical sprites&lt;/li&gt;
&lt;li&gt;VFX color textures&lt;/li&gt;
&lt;li&gt;UI backgrounds and panels&lt;/li&gt;
&lt;li&gt;general RGBA sprites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ASTC does not double in size because a texture has alpha; RGB and RGBA use the same block footprint. But complex color plus complex alpha still has to fit into the same 128 bits, so artifacts can become more visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consider 8x8 or larger for
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;blurred backgrounds&lt;/li&gt;
&lt;li&gt;skies, clouds, smoke, and other low-frequency imagery&lt;/li&gt;
&lt;li&gt;distant assets shown small on screen&lt;/li&gt;
&lt;li&gt;particles where some loss of noise/detail is acceptable&lt;/li&gt;
&lt;li&gt;downloadable content where size pressure is severe&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At 8x8, 10x10, and 12x12, inspect edge smearing, block artifacts, gradients, and alpha transitions carefully. "Background" is not enough justification by itself if the camera can zoom in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting presets by texture role
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Starting point&lt;/th&gt;
&lt;th&gt;What to inspect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;General albedo / sprite&lt;/td&gt;
&lt;td&gt;ASTC 6x6&lt;/td&gt;
&lt;td&gt;Faces, silhouettes, alpha edges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hero character / high-quality UI&lt;/td&gt;
&lt;td&gt;ASTC 4x4-5x5&lt;/td&gt;
&lt;td&gt;Thin lines, eyes, text, gradients&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI background / large panel&lt;/td&gt;
&lt;td&gt;ASTC 6x6-8x8&lt;/td&gt;
&lt;td&gt;Banding, rounded corners, translucency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distant or blurred background&lt;/td&gt;
&lt;td&gt;ASTC 8x8&lt;/td&gt;
&lt;td&gt;Block artifacts when camera approaches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Normal map&lt;/td&gt;
&lt;td&gt;ASTC 4x4-6x6&lt;/td&gt;
&lt;td&gt;Lighting distortion; ensure sRGB is off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packed ORM-like mask&lt;/td&gt;
&lt;td&gt;ASTC 4x4-6x6&lt;/td&gt;
&lt;td&gt;Channel errors and threshold behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Font / SDF&lt;/td&gt;
&lt;td&gt;ASTC 4x4 or compare uncompressed&lt;/td&gt;
&lt;td&gt;Small-size readability and edges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Very small texture&lt;/td&gt;
&lt;td&gt;Prefer atlasing&lt;/td&gt;
&lt;td&gt;Block rounding and wasted mips&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are comparison starting points, not universal answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat normal maps and packed masks more carefully than color
&lt;/h2&gt;

&lt;p&gt;Compression errors in color textures can be visually hidden. Errors in normals and masks can turn into lighting artifacts or threshold changes.&lt;/p&gt;

&lt;p&gt;For normal maps, disable sRGB and use Unity's normal-map import path. Prefer 4x4 or 5x5 when close-up lighting, specular response, or fine normal detail matters; 6x6 can still be sufficient for distant or low-detail surfaces.&lt;/p&gt;

&lt;p&gt;For packed metallic/occlusion/roughness/smoothness masks, remember that channels carry numeric meaning. If a shader branches around a threshold such as 0.5, compression error can change behavior even when the packed image still looks acceptable. Visualize important channels independently during QA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not confuse ASTC LDR and ASTC HDR
&lt;/h2&gt;

&lt;p&gt;The ASTC used for normal PNG/JPEG-derived game textures is primarily LDR ASTC. HDR ASTC has separate hardware requirements.&lt;/p&gt;

&lt;p&gt;On Apple hardware, LDR ASTC starts with the A8-era Apple2 GPU family, while HDR ASTC starts later with the A13-era Apple6 family. On Android, HDR support likewise needs its own OpenGL ES extension or Vulkan capability.&lt;/p&gt;

&lt;p&gt;This implication is unsafe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The device supports ASTC 6x6
therefore
it must support HDR ASTC 6x6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check LDR and HDR separately. HDR fallback can be even more expensive if Unity has to expand into a format such as RGBA Half.&lt;/p&gt;

&lt;h2&gt;
  
  
  Texture compression can affect runtime performance
&lt;/h2&gt;

&lt;p&gt;ASTC is not only about package size. On mobile GPUs, external DRAM traffic is expensive in performance and power. Sampling compressed textures can reduce resident texture memory, memory traffic, and cache pressure.&lt;/p&gt;

&lt;p&gt;Moving from ASTC 4x4 (8 bpp) to 6x6 (~3.56 bpp) cuts the compressed data footprint to about 44%, but that does &lt;strong&gt;not&lt;/strong&gt; imply a similar FPS gain. If the frame is CPU-, shader-, fill-rate-, or synchronization-bound, texture compression may barely change frame time.&lt;/p&gt;

&lt;p&gt;Measure texture memory, bandwidth when available, loading hitches, sustained thermals, and download/install size—not only FPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encoder time is different from runtime decode
&lt;/h2&gt;

&lt;p&gt;Higher-quality ASTC encoding can increase import and build time because the encoder searches harder for a good 128-bit representation. Runtime devices are not recompressing textures every frame; the GPU samples already-compressed blocks.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Block size&lt;/strong&gt; -&amp;gt; memory footprint and quality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoder quality&lt;/strong&gt; -&amp;gt; build time and final compression quality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime sampling&lt;/strong&gt; -&amp;gt; GPU texture hardware&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsupported fallback&lt;/strong&gt; -&amp;gt; possible CPU work and uncompressed memory growth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using faster encoding during development and higher-quality encoding for release is reasonable, but final visual QA should use release-equivalent settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime diagnostics in Unity
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SystemInfo.SupportsTextureFormat&lt;/code&gt; can tell you whether the current device/graphics environment supports a specific &lt;code&gt;TextureFormat&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;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TextureFormatDiagnostics&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="cp"&gt;#if UNITY_EDITOR || DEVELOPMENT_BUILD
&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;RuntimeInitializeOnLoadMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RuntimeInitializeLoadType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AfterAssembliesLoaded&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;LogSupport&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;astc4x4&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SystemInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SupportsTextureFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TextureFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ASTC_4x4&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;astc6x6&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SystemInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SupportsTextureFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TextureFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ASTC_6x6&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;astc8x8&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SystemInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SupportsTextureFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TextureFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ASTC_8x8&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;etc2Rgba&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SystemInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SupportsTextureFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TextureFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ETC2_RGBA8&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;$"GPU=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SystemInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;graphicsDeviceName&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="s"&gt;$"API=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SystemInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;graphicsDeviceType&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="s"&gt;$"ASTC4x4=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;astc4x4&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="s"&gt;$"ASTC6x6=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;astc6x6&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="s"&gt;$"ASTC8x8=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;astc8x8&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="s"&gt;$"ETC2_RGBA8=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;etc2Rgba&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="cp"&gt;#endif
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep this as development/QA diagnostics rather than permanent release logging.&lt;/p&gt;

&lt;p&gt;Also understand what it proves. It tells you about device capability; it does &lt;strong&gt;not&lt;/strong&gt; prove that a specific loaded asset is currently resident as ASTC.&lt;/p&gt;

&lt;p&gt;For actual asset verification, combine it with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the delivered Google Play slice&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Texture2D.format&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;texture.graphicsFormat&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Unity Profiler / Memory Profiler&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you need to distinguish sRGB vs linear, LDR vs HDR, or specific usages, use &lt;code&gt;SystemInfo.IsFormatSupported(GraphicsFormat, GraphicsFormatUsage)&lt;/code&gt; as the more precise API.&lt;/p&gt;

&lt;p&gt;And note that decoding a PNG at runtime with something like &lt;code&gt;LoadImage&lt;/code&gt; does not magically create an ASTC-resident texture. If you need GPU-compressed runtime content, you must ship platform-appropriate compressed data and load it using the correct format.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to verify on real devices
&lt;/h2&gt;

&lt;p&gt;Do not finish ASTC validation in the Editor preview. Test representative iOS hardware, an ASTC Android path, and an ETC2/fallback Android path.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Delivered variant&lt;/strong&gt;: for AABs, confirm which slice reached the device with bundletool, Play Console testing, logs, or distinguishable test assets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime memory&lt;/strong&gt;: use Unity Profiler/Memory Profiler and platform tools. If a texture expected to be ASTC 6x6 occupies RGBA32-like memory, investigate fallback or import settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loading hitches&lt;/strong&gt;: compare controlled builds so format expansion, bundle loading, shader warm-up, and image decoding are not confused with one another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image quality&lt;/strong&gt;: test at real resolution, render scale, mip settings, camera distance, UI scale, and post-processing. Validate the ETC2 slice separately from the ASTC slice.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A practical project policy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  iOS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Treat A8 as the LDR ASTC &lt;strong&gt;hardware&lt;/strong&gt; boundary, not the current minimum shipping device.&lt;/li&gt;
&lt;li&gt;In Unity 6.4, also account for iOS/iPadOS 15+, Deployment Target, Xcode/SDK, and App Store requirements.&lt;/li&gt;
&lt;li&gt;If the actual device set supports ASTC, start general textures at 6x6, important UI/hero assets at 4x4-5x5, and compare 8x8 for low-frequency backgrounds.&lt;/li&gt;
&lt;li&gt;Treat HDR ASTC as a separate capability.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Android / Google Play
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;For phones/tablets, prefer AAB + Texture Compression Targeting with ETC2 as default and ASTC as an additional format.&lt;/li&gt;
&lt;li&gt;Verify both ASTC and ETC2 slices and QA their image quality separately.&lt;/li&gt;
&lt;li&gt;If Google Play Games for PC is also in scope, investigate S3TC separately.&lt;/li&gt;
&lt;li&gt;Do not rely on RGBA32 runtime expansion as a normal shipping path.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Content production
&lt;/h3&gt;

&lt;p&gt;Use 6x6 as an initial preset, not a rule. Manage role-based settings with presets or an &lt;code&gt;AssetPostprocessor&lt;/code&gt;, treat normals/masks/SDFs/tiny textures separately, and perform final QA with release-equivalent encoding while checking memory, loading, and image quality together.&lt;/p&gt;

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

&lt;p&gt;ASTC is one of the most practical standard texture formats for modern mobile GPUs, but the production answer is not "set everything to ASTC 6x6."&lt;/p&gt;

&lt;p&gt;On iOS, A8 is the LDR ASTC hardware boundary, while the actual shipping device set is determined by the intersection of Unity's Player requirements, your Deployment Target, Xcode/SDK constraints, and App Store requirements. On Android, support is fundamentally a GPU/driver capability question, and a portion of the Google Play ecosystem still needs a non-ASTC path.&lt;/p&gt;

&lt;p&gt;That is why ETC2 as the safe default plus ASTC as an additional Google Play Texture Compression Targeting format is a strong general Android policy.&lt;/p&gt;

&lt;p&gt;And ASTC 6x6 being non-power-of-two is not something to fear. Six-by-six describes the texel footprint represented by a &lt;strong&gt;128-bit fixed-size compressed block&lt;/strong&gt;. The real decision is the trade-off between quality, GPU memory, bandwidth, supported devices, and fallback cost.&lt;/p&gt;

&lt;p&gt;Start with 6x6 for general textures, move important assets toward 4x4-5x5, test 8x8 on low-frequency content, and validate both the delivered format and the real memory/quality result on devices. That gives you the benefits of ASTC without turning format support or aggressive compression into a hidden production risk.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/Manual/texture-choose-format-by-platform.html" rel="noopener noreferrer"&gt;Unity 6.4 Manual: Choose a GPU texture format by platform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/Manual/system-requirements.html" rel="noopener noreferrer"&gt;Unity 6.4 Manual: System requirements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/Manual/android-distribution-google-play.html" rel="noopener noreferrer"&gt;Unity 6.4 Manual: Google Play delivery requirements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/ScriptReference/SystemInfo.SupportsTextureFormat.html" rel="noopener noreferrer"&gt;Unity 6.4 Scripting API: SystemInfo.SupportsTextureFormat&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/ScriptReference/SystemInfo.IsFormatSupported.html" rel="noopener noreferrer"&gt;Unity 6.4 Scripting API: SystemInfo.IsFormatSupported&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/ScriptReference/TextureImporterFormat.html" rel="noopener noreferrer"&gt;Unity 6.4 Scripting API: TextureImporterFormat&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/guide/playcore/asset-delivery/texture-compression" rel="noopener noreferrer"&gt;Android Developers: Target texture compression formats in Android App Bundles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf" rel="noopener noreferrer"&gt;Apple: Metal Feature Set Tables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.khronos.org/OpenGL/specs/es/3.2/es_spec_3.2.pdf" rel="noopener noreferrer"&gt;Khronos: OpenGL ES 3.2 Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.vulkan.org/spec/latest/chapters/features.html" rel="noopener noreferrer"&gt;Khronos Vulkan Specification: Features&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.vulkan.org/refpages/latest/refpages/source/VK_EXT_texture_compression_astc_hdr.html" rel="noopener noreferrer"&gt;Khronos Vulkan: VK_EXT_texture_compression_astc_hdr&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.khronos.org/OpenGL/extensions/KHR/KHR_texture_compression_astc_hdr.txt" rel="noopener noreferrer"&gt;Khronos: KHR_texture_compression_astc_hdr&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ARM-software/astc-encoder/blob/main/Docs/FormatOverview.md" rel="noopener noreferrer"&gt;Arm ASTC Encoder: Format overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.arm.com/-/media/Arm%20Developer%20Community/PDF/ASTC_User_Guide_102162_0001_01.pdf" rel="noopener noreferrer"&gt;Arm: Adaptive Scalable Texture Compression User Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Using External C# DLLs Safely in Unity: Newer C#, IL2CPP, Builds, and Debugging</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Fri, 07 Aug 2026 12:50:12 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/using-external-c-dlls-safely-in-unity-newer-c-il2cpp-builds-and-debugging-2d68</link>
      <guid>https://dev.to/gamedevtoollab/using-external-c-dlls-safely-in-unity-newer-c-il2cpp-builds-and-debugging-2d68</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Part 1 covered the adoption decision: the difference between an external DLL and an asmdef, sharing code with a C# game server, reducing Unity's daily compilation scope, running standard .NET tests, and keeping Unity dependencies out of the core domain.&lt;/p&gt;

&lt;p&gt;This second part focuses on implementation and operations. It shows how to build a managed DLL with an external .NET SDK and integrate it safely into Unity.&lt;/p&gt;

&lt;p&gt;The main topics are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a DLL can use newer C# syntax than Unity compiles directly.&lt;/li&gt;
&lt;li&gt;The different roles of &lt;code&gt;netstandard2.1&lt;/code&gt; and &lt;code&gt;LangVersion&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Pinning the .NET SDK with &lt;code&gt;global.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Building a public API that still passes warnings-as-errors and XML documentation checks.&lt;/li&gt;
&lt;li&gt;Referencing the same DLL from Unity and a C# server.&lt;/li&gt;
&lt;li&gt;Managing Plugin Importer settings and &lt;code&gt;.meta&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;IL2CPP, AOT, managed stripping, reflection, and generic-code restrictions.&lt;/li&gt;
&lt;li&gt;Portable PDB, Source Link, commit hashes, and reproducible debugging.&lt;/li&gt;
&lt;li&gt;Treating the DLL, PDB, XML documentation, &lt;code&gt;.meta&lt;/code&gt;, license, and linker configuration as one deliverable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The examples are based on Unity 6.5 as of July 2026. The goal is not to bypass Unity's runtime restrictions. The goal is to use an external build toolchain while staying inside the runtime and API surface Unity can actually execute.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Main Rule: A Newer Compiler Does Not Give Unity a Newer .NET Runtime
&lt;/h2&gt;

&lt;p&gt;Building a DLL with an external .NET SDK can let you use C# syntax that the Unity Editor does not accept as project source. Three separate layers must still be considered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The C# compiler that understands the source syntax.&lt;/li&gt;
&lt;li&gt;The Unity runtime that executes the generated IL.&lt;/li&gt;
&lt;li&gt;The Base Class Library APIs called by the DLL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Setting &lt;code&gt;LangVersion&lt;/code&gt; to C# 12 does not turn Unity into a &lt;code&gt;.NET 10&lt;/code&gt; runtime. The DLL imported into Unity must still target a compatible API surface, typically &lt;code&gt;netstandard2.1&lt;/code&gt; for the Unity version considered in this article.&lt;/p&gt;

&lt;p&gt;A safe design also keeps newer syntax inside the implementation and exposes conservative public types: ordinary classes, structs, interfaces, arrays, and other APIs that Unity-side source can consume without depending on the new syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  You Can Use Newer C# Syntax Inside the DLL
&lt;/h2&gt;

&lt;p&gt;The important distinction is straightforward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If an external compiler can compile the source and the resulting assembly stays within Unity's runtime and API compatibility limits, Unity can often use the DLL even when the Editor could not compile that source directly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unity 6.5 documents C# 9.0 as the language version used for project scripts, with some feature restrictions. The exact status by Unity version is documented in &lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/csharp-compiler.html" rel="noopener noreferrer"&gt;Unity's C# compiler and language version reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The source of an imported DLL is not parsed by Unity's C# compiler. For example, a recent Roslyn compiler can compile a C# 12 primary constructor into IL:&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;Shared.GameRules&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Keep the C# 12 primary constructor inside the DLL implementation.&lt;/span&gt;
&lt;span class="k"&gt;internal&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;StaminaRuleCore&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;maxValue&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;recoverySeconds&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;MaxValue&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;maxValue&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;RecoverySeconds&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;recoverySeconds&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;Clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clamp&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="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxValue&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;Unity loads the compiled assembly rather than parsing this source file.&lt;/p&gt;

&lt;p&gt;There is no need to make this type public. The implementation later in this article keeps the primary-constructor type &lt;code&gt;internal&lt;/code&gt; and exposes a conventional &lt;code&gt;StaminaRule&lt;/code&gt; facade to Unity and the server. That keeps the architectural rule consistent: newer syntax is an implementation detail, while the public boundary remains conservative.&lt;/p&gt;

&lt;p&gt;This does &lt;strong&gt;not&lt;/strong&gt; mean that every new C# or .NET feature works automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three Compatibility Layers
&lt;/h3&gt;

&lt;p&gt;At minimum, verify these three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Compiler compatibility&lt;/strong&gt;: Does the selected Roslyn compiler understand the source syntax?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime compatibility&lt;/strong&gt;: Can Unity's scripting backend execute the resulting IL and metadata?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API compatibility&lt;/strong&gt;: Are all referenced framework APIs available in Unity's supported profile and on every target platform?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Some language features, such as file-scoped namespaces and raw string literals, mostly compile down to forms that older runtimes already understand.&lt;/p&gt;

&lt;p&gt;Other features require new runtime capabilities, metadata, attributes, or library types. Microsoft also notes that some language features introduced from C# 8 onward depend on CLR capabilities or types associated with newer .NET implementations.&lt;/p&gt;

&lt;p&gt;Unity 6.5 supports managed plug-ins targeting compatible .NET Standard or .NET Framework profiles. &lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/dotnet-profile-support.html" rel="noopener noreferrer"&gt;Unity's API compatibility documentation&lt;/a&gt; states that assemblies targeting .NET Core are not the intended input for this workflow.&lt;/p&gt;

&lt;p&gt;Therefore, this is not an appropriate Unity plug-in target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;net10.0&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not expect to import that server-targeted assembly directly into Unity.&lt;/p&gt;

&lt;p&gt;Use a compatibility target for the shared library instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&lt;/span&gt; &lt;span class="na"&gt;Sdk=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Sdk"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;netstandard2.1&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;LangVersion&amp;gt;&lt;/span&gt;12.0&lt;span class="nt"&gt;&amp;lt;/LangVersion&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Nullable&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/Nullable&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ImplicitUsings&amp;gt;&lt;/span&gt;disable&lt;span class="nt"&gt;&amp;lt;/ImplicitUsings&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TreatWarningsAsErrors&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/TreatWarningsAsErrors&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Deterministic&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/Deterministic&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TargetFramework&lt;/code&gt; defines the available API surface and runtime compatibility. &lt;code&gt;LangVersion&lt;/code&gt; defines the source syntax accepted by the compiler. Treating them as separate decisions is essential.&lt;/p&gt;

&lt;h3&gt;
  
  
  Confirm Unity's API Compatibility Level
&lt;/h3&gt;

&lt;p&gt;Successfully compiling for &lt;code&gt;netstandard2.1&lt;/code&gt; does not prove that every Unity target will behave identically.&lt;/p&gt;

&lt;p&gt;Pin and test the following assumptions for each artifact:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The supported Unity version or version range.&lt;/li&gt;
&lt;li&gt;The Player Settings &lt;code&gt;API Compatibility Level&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The actual platforms: Android, iOS, WebGL, desktop, consoles, and others.&lt;/li&gt;
&lt;li&gt;Every Base Class Library API used inside the DLL.&lt;/li&gt;
&lt;li&gt;The scripting backend, AOT restrictions, and managed stripping configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An API can exist in the chosen target framework and still require validation on the actual Unity backend or platform. If the SDK must support several Unity versions, consider targeting the oldest supported API surface, publishing different artifacts per compatibility group, or using multiple target frameworks where that genuinely helps.&lt;/p&gt;

&lt;p&gt;Use &lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/dotnet-profile-support.html" rel="noopener noreferrer"&gt;Unity's API compatibility levels for .NET&lt;/a&gt; as the reference for the project setting and supported profiles.&lt;/p&gt;

&lt;p&gt;Avoid this in a shared library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;LangVersion&amp;gt;&lt;/span&gt;latest&lt;span class="nt"&gt;&amp;lt;/LangVersion&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;latest&lt;/code&gt; can change behavior merely because a developer machine or CI image installs a newer SDK. Use an explicit version such as &lt;code&gt;12.0&lt;/code&gt;, and pin the SDK itself with &lt;code&gt;global.json&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features That Are Usually Easier, and Features That Need More Care
&lt;/h3&gt;

&lt;p&gt;Features that often compile into existing IL and metadata patterns include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File-scoped namespaces.&lt;/li&gt;
&lt;li&gt;Global using directives.&lt;/li&gt;
&lt;li&gt;Raw string literals.&lt;/li&gt;
&lt;li&gt;Newer pattern matching syntax.&lt;/li&gt;
&lt;li&gt;Primary constructors.&lt;/li&gt;
&lt;li&gt;Lambda and type-inference syntax improvements.&lt;/li&gt;
&lt;li&gt;Compile-time code generation helpers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Features and libraries that deserve more careful validation include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;required&lt;/code&gt; members.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;init&lt;/code&gt; accessors.&lt;/li&gt;
&lt;li&gt;Features that require newer attribute types.&lt;/li&gt;
&lt;li&gt;Default interface methods and other runtime-dependent features.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;System.Reflection.Emit&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;APIs specific to a newer .NET runtime.&lt;/li&gt;
&lt;li&gt;Libraries that assume CoreCLR or NativeAOT behavior.&lt;/li&gt;
&lt;li&gt;Reflection-heavy serializers and DI containers.&lt;/li&gt;
&lt;li&gt;Runtime proxy or code generation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The risk increases when the &lt;strong&gt;public API&lt;/strong&gt; exposes metadata or construction requirements that Unity-side code must understand.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;Use newer C# syntax inside the DLL, but keep the types, parameters, return values, and attributes exposed to Unity conservative.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, the internal code may use a primary constructor and new pattern syntax, while the public API uses ordinary classes, structs, interfaces, &lt;code&gt;Task&lt;/code&gt;, arrays, and &lt;code&gt;IReadOnlyList&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Be cautious about exposing &lt;code&gt;required&lt;/code&gt;, &lt;code&gt;init&lt;/code&gt;, or new attribute-dependent initialization patterns on public DTOs that Unity code must construct or serialize. Also distinguish between types used as a callable API and types expected to participate in Unity serialization.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;global using&lt;/code&gt; is usually compatible at the IL level, but excessive use can make source dependencies less visible. Keep its use consistent through team conventions and analyzers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Not Make New Syntax the Only Reason for DLL Packaging
&lt;/h3&gt;

&lt;p&gt;If the sole goal is to use a C# 12 syntax feature, the operational cost can exceed the benefit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Navigating between Unity source and external library source.&lt;/li&gt;
&lt;li&gt;Running a separate build.&lt;/li&gt;
&lt;li&gt;Keeping DLL and PDB files synchronized.&lt;/li&gt;
&lt;li&gt;Maintaining CI compatibility tests.&lt;/li&gt;
&lt;li&gt;Verifying IL2CPP and real devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat newer syntax as an additional benefit of a library that already has stronger reasons to exist: server sharing, independent tests, strict architecture, or binary distribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the Standard .NET SDK Toolchain
&lt;/h2&gt;

&lt;p&gt;An SDK-style project outside Unity can use the normal .NET development toolchain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shared settings through &lt;code&gt;Directory.Build.props&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Warnings as errors.&lt;/li&gt;
&lt;li&gt;Nullable Reference Types.&lt;/li&gt;
&lt;li&gt;Roslyn analyzers.&lt;/li&gt;
&lt;li&gt;Source Generators.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dotnet test&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dotnet format&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dotnet pack&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;NuGet dependency management.&lt;/li&gt;
&lt;li&gt;Deterministic builds.&lt;/li&gt;
&lt;li&gt;Source Link.&lt;/li&gt;
&lt;li&gt;Multiple target frameworks.&lt;/li&gt;
&lt;li&gt;CI caching.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unity can also use analyzers and additional compiler arguments, but its exact behavior depends on the Unity version and generated project setup. A separate library can follow the same rules used by the server or platform team.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pin the SDK with &lt;code&gt;global.json&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Once the compiler is external, prevent developers and CI from silently using different Roslyn versions.&lt;/p&gt;

&lt;p&gt;Place a &lt;code&gt;global.json&lt;/code&gt; near the repository root:&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;"sdk"&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;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.0.301"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rollForward"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"latestPatch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowPrerelease"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;&lt;strong&gt;&lt;code&gt;10.0.301&lt;/code&gt; is an example for this article. It is not a required SDK for Unity DLL development. Replace it with an SDK version installed and supported by your team and CI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;latestPatch&lt;/code&gt; permits patch updates within the same feature band while avoiding an unintended feature-band or major-version jump.&lt;/p&gt;

&lt;p&gt;Print the environment at the start of CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet &lt;span class="nt"&gt;--version&lt;/span&gt;
dotnet &lt;span class="nt"&gt;--info&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the commit hash to &lt;code&gt;InformationalVersion&lt;/code&gt; so that both build logs and the produced assembly identify the compiler environment and source revision.&lt;/p&gt;

&lt;p&gt;If the library should treat &lt;strong&gt;all warnings as errors&lt;/strong&gt;, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Nullable&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/Nullable&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;TreatWarningsAsErrors&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/TreatWarningsAsErrors&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If only nullable warnings should fail the build, remove &lt;code&gt;TreatWarningsAsErrors&lt;/code&gt; and configure &lt;code&gt;WarningsAsErrors&lt;/code&gt; for the desired warning IDs or categories. Avoid showing both approaches without explaining which policy is intended.&lt;/p&gt;

&lt;p&gt;Source Generators can also run entirely in the external build, with Unity receiving only the resulting DLL. This can be easier to reproduce than installing the generator into Unity's compiler pipeline.&lt;/p&gt;

&lt;p&gt;Generated code is still subject to Unity's runtime, API compatibility, and IL2CPP restrictions. External generation controls the toolchain; it does not bypass the platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation: One DLL Used by Unity and a .NET Server
&lt;/h2&gt;

&lt;p&gt;The following is a small but complete layout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Directory Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GameRepository/
├── global.json
├── Shared/
│   ├── Shared.GameRules/
│   │   ├── Shared.GameRules.csproj
│   │   └── StaminaRule.cs
│   └── Shared.GameRules.Tests/
├── Server/
│   └── Game.Server/
└── UnityClient/
    └── Assets/
        └── Plugins/
            └── Company/
                ├── Shared.GameRules.dll
                ├── Shared.GameRules.dll.meta
                └── Shared.GameRules.pdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treat more than the DLL itself as the artifact. A production package may include the DLL, PDB, XML documentation, license, version metadata, and, where necessary, a &lt;code&gt;link.xml&lt;/code&gt; file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared Library &lt;code&gt;csproj&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&lt;/span&gt; &lt;span class="na"&gt;Sdk=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Sdk"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;netstandard2.1&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;LangVersion&amp;gt;&lt;/span&gt;12.0&lt;span class="nt"&gt;&amp;lt;/LangVersion&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Nullable&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/Nullable&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ImplicitUsings&amp;gt;&lt;/span&gt;disable&lt;span class="nt"&gt;&amp;lt;/ImplicitUsings&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TreatWarningsAsErrors&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/TreatWarningsAsErrors&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;GenerateDocumentationFile&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/GenerateDocumentationFile&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Deterministic&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/Deterministic&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;GenerateDocumentationFile&lt;/code&gt; and &lt;code&gt;TreatWarningsAsErrors&lt;/code&gt; enabled together, missing XML documentation for public types and members can make CS1591 fail the build. Add documentation to the public API rather than suppressing the warning in a reusable SDK.&lt;/p&gt;

&lt;p&gt;A short tutorial could disable CS1591, but an internal shared library or distributed SDK benefits from shipping API documentation with the artifact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep C# 12 in the Internal Implementation
&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;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;namespace&lt;/span&gt; &lt;span class="nn"&gt;Shared.GameRules&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// Defines the maximum stamina and time-based recovery rule.&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;/summary&amp;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;StaminaRule&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;StaminaRuleCore&lt;/span&gt; &lt;span class="n"&gt;_core&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Initializes a stamina rule.&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;param name="maxValue"&amp;gt;The maximum stamina value.&amp;lt;/param&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;param name="recoverySeconds"&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// The number of seconds required to recover one point.&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/param&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;StaminaRule&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;maxValue&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;recoverySeconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_core&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;StaminaRuleCore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recoverySeconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Gets the maximum stamina value.&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;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;MaxValue&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Gets the seconds required to recover one point.&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;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;RecoverySeconds&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecoverySeconds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Clamps a value to the inclusive range from zero to the maximum.&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;param name="value"&amp;gt;The value to clamp.&amp;lt;/param&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;returns&amp;gt;The clamped value.&amp;lt;/returns&amp;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;Clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&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="n"&gt;_core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clamp&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="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Applies a non-negative recovery amount without exceeding the maximum.&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;param name="currentValue"&amp;gt;The stamina value before recovery.&amp;lt;/param&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;param name="recoveredAmount"&amp;gt;The non-negative amount to apply.&amp;lt;/param&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;returns&amp;gt;The stamina value after recovery.&amp;lt;/returns&amp;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;ApplyRecovery&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;currentValue&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;recoveredAmount&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;_core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ApplyRecovery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recoveredAmount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Calculates the stamina recovered during the specified UTC interval.&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;param name="previous"&amp;gt;The previous confirmed recovery time in UTC.&amp;lt;/param&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;param name="current"&amp;gt;The current calculation time in UTC.&amp;lt;/param&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;returns&amp;gt;The amount recovered during the interval.&amp;lt;/returns&amp;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;CalculateRecoveredAmount&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;previous&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;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;return&lt;/span&gt; &lt;span class="n"&gt;_core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CalculateRecoveredAmount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previous&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;span class="c1"&gt;// The primary constructor is an internal implementation detail.&lt;/span&gt;
&lt;span class="k"&gt;internal&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;StaminaRuleCore&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;maxValue&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;recoverySeconds&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;MaxValue&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="nf"&gt;ValidatePositive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxValue&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;maxValue&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;RecoverySeconds&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="nf"&gt;ValidatePositive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recoverySeconds&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;recoverySeconds&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;Clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&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="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clamp&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="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MaxValue&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="nf"&gt;ApplyRecovery&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;currentValue&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;recoveredAmount&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;recoveredAmount&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentOutOfRangeException&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;recoveredAmount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;recoveredAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"The recovered amount must be zero or greater."&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;normalizedCurrent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentValue&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;finalValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;normalizedCurrent&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;recoveredAmount&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;finalValue&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;MaxValue&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;MaxValue&lt;/span&gt;
            &lt;span class="p"&gt;:&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="n"&gt;finalValue&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="nf"&gt;CalculateRecoveredAmount&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;previous&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;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;if&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;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;previous&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="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;elapsed&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;previous&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;elapsedSeconds&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;Ticks&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;TicksPerSecond&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;recoveredAmount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;elapsedSeconds&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;RecoverySeconds&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;recoveredAmount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&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;MaxValue&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="n"&gt;MaxValue&lt;/span&gt;
            &lt;span class="p"&gt;:&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="n"&gt;recoveredAmount&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;int&lt;/span&gt; &lt;span class="nf"&gt;ValidatePositive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="k"&gt;value&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;parameterName&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;value&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="k"&gt;value&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;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;parameterName&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="s"&gt;"The value must be greater than zero."&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 public &lt;code&gt;StaminaRule&lt;/code&gt; uses a normal constructor and ordinary methods. Unity-side C# source does not need to understand the C# 12 primary constructor.&lt;/p&gt;

&lt;p&gt;The method is named &lt;code&gt;CalculateRecoveredAmount&lt;/code&gt; because it returns the amount recovered during an interval, not the final stamina value. Apply the result through the separate public API:&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;finalValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ApplyRecovery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;currentValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;recoveredAmount&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;ApplyRecovery&lt;/code&gt; normalizes the current value, widens the addition to &lt;code&gt;long&lt;/code&gt;, and saturates at &lt;code&gt;MaxValue&lt;/code&gt;. It avoids overflowing an &lt;code&gt;int&lt;/code&gt; before calling &lt;code&gt;Clamp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The time calculation also avoids &lt;code&gt;TimeSpan.TotalSeconds&lt;/code&gt; and its &lt;code&gt;double&lt;/code&gt; representation. It converts ticks to whole seconds explicitly, making the rounding policy consistent across consumers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server Side
&lt;/h3&gt;

&lt;p&gt;During development, reference the project directly from the server instead of manually copying the DLL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ProjectReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"..\..\Shared\Shared.GameRules\Shared.GameRules.csproj"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server can still target &lt;code&gt;net10.0&lt;/code&gt;; only the shared project targets &lt;code&gt;netstandard2.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Do not scatter direct clock reads across domain code. Inject a clock:&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;Shared.GameRules&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;internal&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;internal&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;SystemClock&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="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;UtcNow&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;internal&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;StaminaRecoveryService&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;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;StaminaRule&lt;/span&gt; &lt;span class="n"&gt;_rule&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;StaminaRecoveryService&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="n"&gt;_rule&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;StaminaRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;maxValue&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;recoverySeconds&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="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="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;lastRecoveredAt&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;_rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CalculateRecoveredAmount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;lastRecoveredAt&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;UtcNow&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;Even in a short sample, a clock abstraction makes fixed-time tests possible. The server uses UTC as the authoritative source and does not trust the client device clock for the final decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unity Side
&lt;/h3&gt;

&lt;p&gt;Place the built assembly under a stable path such as &lt;code&gt;Assets/Plugins/Company&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;Shared.GameRules&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;StaminaPresenter&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;maxValue&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="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;recoverySeconds&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="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;StaminaRule&lt;/span&gt; &lt;span class="n"&gt;_rule&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;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;_rule&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;StaminaRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recoverySeconds&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="nf"&gt;Normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&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="n"&gt;_rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clamp&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unity lifecycle and Inspector values remain in a thin adapter; the calculation lives in the shared DLL.&lt;/p&gt;

&lt;p&gt;The field uses &lt;code&gt;null!&lt;/code&gt; because Unity initializes it in &lt;code&gt;Awake&lt;/code&gt;, while a nullable-enabled compiler cannot infer that lifecycle guarantee. If the design permits access before &lt;code&gt;Awake&lt;/code&gt;, use a nullable field, a defensive exception, or another initialization pattern consistent with the team's Unity conventions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automate DLL, PDB, and XML Copying
&lt;/h3&gt;

&lt;p&gt;Manual copying makes it easy to commit an old DLL with a new source revision. Automate at least the basic flow.&lt;/p&gt;

&lt;p&gt;PowerShell example:&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="bp"&gt;$Error&lt;/span&gt;&lt;span class="n"&gt;ActionPreference&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="s2"&gt;"Stop"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build&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="s2"&gt;".\Shared\Shared.GameRules\Shared.GameRules.csproj"&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;-c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Release&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;test&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="s2"&gt;".\Shared\Shared.GameRules.Tests\Shared.GameRules.Tests.csproj"&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;-c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Release&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$source&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="s2"&gt;".\Shared\Shared.GameRules\bin\Release\netstandard2.1"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$destination&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="s2"&gt;".\UnityClient\Assets\Plugins\Company"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;New-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ItemType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$destination&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="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Copy-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="s2"&gt;\Shared.GameRules.dll"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Copy-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="s2"&gt;\Shared.GameRules.pdb"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Copy-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="s2"&gt;\Shared.GameRules.xml"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script intentionally does not overwrite the &lt;code&gt;.meta&lt;/code&gt; file. Commit or package the configured &lt;code&gt;Shared.GameRules.dll.meta&lt;/code&gt;, and replace only the DLL at the same path.&lt;/p&gt;

&lt;p&gt;A CI pipeline can use this order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet --version / dotnet --info
dotnet restore
dotnet build
dotnet test
Copy DLL, PDB, and XML documentation into a Unity Package
Run Unity EditMode tests
Build the target Player
Run an IL2CPP smoke test on the target platform
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful external build proves only that the .NET project compiles. It does not prove that Unity import, IL2CPP, managed stripping, or the final device works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unity Plugin Importer Settings
&lt;/h2&gt;

&lt;p&gt;Unity treats a managed DLL as a plug-in asset. The main settings are managed through the Plugin Inspector.&lt;/p&gt;

&lt;h3&gt;
  
  
  Store Importer Settings in &lt;code&gt;.meta&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Auto Referenced&lt;/code&gt;, target platforms, CPU, editor-only settings, and other Plugin Importer state are stored in Unity's &lt;code&gt;.meta&lt;/code&gt; file, not inside the DLL.&lt;/p&gt;

&lt;p&gt;If CI copies a DLL to a new path and lets Unity generate a fresh &lt;code&gt;.meta&lt;/code&gt; every time, the import configuration is no longer reproducible.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Configure the Plugin Importer once in Unity.&lt;/li&gt;
&lt;li&gt;Commit or package the resulting &lt;code&gt;Shared.GameRules.dll.meta&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Replace only the DLL at the same path during updates.&lt;/li&gt;
&lt;li&gt;Include the configured &lt;code&gt;.meta&lt;/code&gt; when distributing the package to a new project.&lt;/li&gt;
&lt;li&gt;Review the platform settings for every platform-specific DLL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The importer configuration is part of the deliverable, not local editor state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto Referenced
&lt;/h3&gt;

&lt;p&gt;When enabled, Unity assemblies can reference the DLL automatically. This is convenient for initial integration but broadens the dependency graph.&lt;/p&gt;

&lt;p&gt;For a small project with one foundational DLL, leaving it enabled may be reasonable. As the number of libraries grows, disabling it and referencing the DLL only from selected asmdef files makes dependencies clearer.&lt;/p&gt;

&lt;p&gt;A predefined assembly such as &lt;code&gt;Assembly-CSharp&lt;/code&gt; cannot configure an explicit reference to a precompiled DLL. Code that needs a DLL with &lt;code&gt;Auto Referenced&lt;/code&gt; disabled must live under an asmdef that references the DLL. If existing code remains in &lt;code&gt;Assembly-CSharp&lt;/code&gt;, the DLL must remain auto-referenced.&lt;/p&gt;

&lt;h3&gt;
  
  
  Platform Settings
&lt;/h3&gt;

&lt;p&gt;Select the platforms that should include the DLL: Editor, Windows, macOS, Android, iOS, WebGL, and others.&lt;/p&gt;

&lt;p&gt;A pure &lt;code&gt;netstandard2.1&lt;/code&gt; library is often usable on all managed targets, but a DLL calling OS-specific APIs must be restricted.&lt;/p&gt;

&lt;p&gt;If several platform-specific files share the same assembly name, ensure that only one is active for a given build target.&lt;/p&gt;

&lt;h3&gt;
  
  
  Override References on asmdef Files
&lt;/h3&gt;

&lt;p&gt;For an asmdef that consumes a DLL whose &lt;code&gt;Auto Referenced&lt;/code&gt; setting is disabled, enable &lt;code&gt;Override References&lt;/code&gt; and add the DLL under &lt;code&gt;Assembly References&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once &lt;code&gt;Override References&lt;/code&gt; is enabled, the asmdef explicitly lists the precompiled DLLs it references. Forgetting another dependency can therefore produce compilation errors.&lt;/p&gt;

&lt;p&gt;Adopt the setting gradually on assemblies where narrowing the dependency graph has a real benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  IL2CPP and AOT Restrictions
&lt;/h2&gt;

&lt;p&gt;A DLL that works in the Mono Editor is not guaranteed to work on iOS, consoles, WebGL, or IL2CPP Android.&lt;/p&gt;

&lt;p&gt;Unity documents AOT restrictions, the lack of &lt;code&gt;System.Reflection.Emit&lt;/code&gt;, stripping risks for code referenced only through reflection, and special considerations for serializers and generic code. See &lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/scripting-restrictions.html" rel="noopener noreferrer"&gt;Unity's IL2CPP limitations&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reflection and Managed Stripping
&lt;/h3&gt;

&lt;p&gt;A string-based type lookup is difficult for static analysis to follow:&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;// Simplified example showing a stripping risk.&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;typeName&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;$"Type was not found: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;typeName&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;var&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Activator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&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 a minimal illustration of the risk, not a complete production factory. It may work in the Editor while the type or constructor is removed from an IL2CPP Player. See &lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/managed-code-stripping.html" rel="noopener noreferrer"&gt;Unity's Managed code stripping documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;Preserve&lt;/code&gt; or &lt;code&gt;link.xml&lt;/code&gt; where necessary. There is an architectural detail, however: applying &lt;code&gt;UnityEngine.Scripting.Preserve&lt;/code&gt; directly inside a Unity-independent Core DLL introduces a reference to &lt;code&gt;UnityEngine&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the Core DLL must remain a pure .NET library, prefer one of these approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Put the preservation rule in the Unity project's &lt;code&gt;link.xml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Put Unity-specific preservation declarations in a Unity adapter assembly.&lt;/li&gt;
&lt;li&gt;Replace reflection with explicit static registration or generated code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example &lt;code&gt;link.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;linker&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;assembly&lt;/span&gt; &lt;span class="na"&gt;fullname=&lt;/span&gt;&lt;span class="s"&gt;"Shared.GameRules"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;type&lt;/span&gt; &lt;span class="na"&gt;fullname=&lt;/span&gt;&lt;span class="s"&gt;"Shared.GameRules.GeneratedRuleRegistry"&lt;/span&gt; &lt;span class="na"&gt;preserve=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/assembly&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/linker&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not preserve an entire large assembly without a reason. It can increase build size and IL2CPP build time. Reduce reflection and preserve the smallest necessary surface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime Code Generation
&lt;/h3&gt;

&lt;p&gt;Be careful with libraries that depend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;System.Reflection.Emit&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DynamicMethod&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Runtime proxy generation.&lt;/li&gt;
&lt;li&gt;JIT-based serializers.&lt;/li&gt;
&lt;li&gt;Runtime compilation of expression trees.&lt;/li&gt;
&lt;li&gt;Dynamically generated DI interceptors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They may work in the Mono Editor and fail in an AOT environment. Prefer libraries that provide an AOT mode, source generation, pre-registration, or static resolvers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generic Code
&lt;/h3&gt;

&lt;p&gt;AOT compilation must know which concrete generic combinations are required.&lt;/p&gt;

&lt;p&gt;Generic methods or types reached only through reflection are easy to miss. Possible mitigations include explicit references to representative combinations, generated registration code, or linker configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test the Final Target
&lt;/h3&gt;

&lt;p&gt;A useful validation order is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Normal .NET tests.&lt;/li&gt;
&lt;li&gt;Unity Editor EditMode tests.&lt;/li&gt;
&lt;li&gt;Mono Development Build.&lt;/li&gt;
&lt;li&gt;IL2CPP Development Build.&lt;/li&gt;
&lt;li&gt;The real Android, iOS, WebGL, console, or desktop target.&lt;/li&gt;
&lt;li&gt;A Release Build using the production Managed Stripping Level.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;"It compiles in Unity" is only the first checkpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve Debuggability
&lt;/h2&gt;

&lt;p&gt;A common complaint about external DLLs is that exception stack traces no longer navigate to source lines.&lt;/p&gt;

&lt;p&gt;Place the Portable PDB produced by the same build next to the DLL, and make the corresponding source revision available to developers.&lt;/p&gt;

&lt;p&gt;Portable PDB mainly improves managed-code debugging and mapping managed exception stack traces to source. An IL2CPP Player's native crash analysis is a different pipeline. It can require Development Build or Script Debugging settings during investigation, IL2CPP-generated native symbols, and platform-specific symbol storage or upload. A managed PDB alone does not solve every Player crash.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Assets/Plugins/Company/
├── Shared.GameRules.dll
└── Shared.GameRules.pdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never update the DLL while leaving an old PDB, and do not combine artifacts from separate builds. Publish them together under one CI build number.&lt;/p&gt;

&lt;p&gt;Useful practices for an internal library include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configure Source Link so compatible IDEs and debuggers can retrieve the matching source.&lt;/li&gt;
&lt;li&gt;Put the commit hash in &lt;code&gt;InformationalVersion&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Log the library version during application startup.&lt;/li&gt;
&lt;li&gt;Prevent accidental mixing of Debug and Release artifacts.&lt;/li&gt;
&lt;li&gt;Define how NuGet/package versions, assembly versions, and file versions relate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An exception report containing &lt;code&gt;Shared.GameRules 2.3.1+abc1234&lt;/code&gt; immediately identifies the source revision to inspect.&lt;/p&gt;

&lt;p&gt;DLL packaging does not inherently make debugging impossible. Debugging becomes difficult when binary, symbol, source, and version relationships are not automated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Validation Before Adoption or Release
&lt;/h2&gt;

&lt;p&gt;Before using the article's configuration in a production project, verify at least the following in a minimal repository:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;dotnet build -c Release&lt;/code&gt; for the shared library.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dotnet test -c Release&lt;/code&gt; for the shared tests.&lt;/li&gt;
&lt;li&gt;A server sample with the &lt;code&gt;ProjectReference&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Unity import after copying the DLL, PDB, and XML documentation.&lt;/li&gt;
&lt;li&gt;Compilation with DLL &lt;code&gt;Auto Referenced&lt;/code&gt; disabled and asmdef &lt;code&gt;Override References&lt;/code&gt; configured.&lt;/li&gt;
&lt;li&gt;Unity EditMode tests.&lt;/li&gt;
&lt;li&gt;An IL2CPP Development Build for the target platform.&lt;/li&gt;
&lt;li&gt;A Release Build using a production-like Managed Stripping Level.&lt;/li&gt;
&lt;li&gt;Device smoke tests for reflection, serialization, and generic-code paths.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A successful external build does not validate Unity import, IL2CPP, stripping, or platform APIs. Automate as much of the full path as practical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation and Operations Decision Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Goal or situation&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Use C# syntax newer than Unity compiles directly&lt;/td&gt;
&lt;td&gt;Possible in an external DLL, but it should be a secondary benefit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import a &lt;code&gt;net8.0&lt;/code&gt; or &lt;code&gt;net10.0&lt;/code&gt; server DLL directly into Unity&lt;/td&gt;
&lt;td&gt;Do not; separate a Unity-compatible target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use &lt;code&gt;LangVersion latest&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Prefer an explicit numeric version for reproducibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expose new-language features throughout the public API&lt;/td&gt;
&lt;td&gt;Keep them internal and use a conservative public surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use a Reflection.Emit-based library on iOS&lt;/td&gt;
&lt;td&gt;Avoid it or find an AOT-compatible mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distribute the same DLL to several games&lt;/td&gt;
&lt;td&gt;A versioned UPM package is a strong option&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ship Asset Store implementation as a binary&lt;/td&gt;
&lt;td&gt;A DLL can work, but it remains decompilable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share Unity-heavy code with a server&lt;/td&gt;
&lt;td&gt;Split Core and Unity Adapter assemblies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Make Plugin Importer settings reproducible&lt;/td&gt;
&lt;td&gt;Version the &lt;code&gt;.meta&lt;/code&gt; file with the artifact&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;The key practices for operating external C# DLLs safely in Unity are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Treat &lt;code&gt;LangVersion&lt;/code&gt; and &lt;code&gt;TargetFramework&lt;/code&gt; as separate compatibility decisions.&lt;/li&gt;
&lt;li&gt;Keep newer C# syntax inside the implementation and expose a conservative API.&lt;/li&gt;
&lt;li&gt;Target a Unity-compatible surface such as &lt;code&gt;netstandard2.1&lt;/code&gt; for the shared DLL.&lt;/li&gt;
&lt;li&gt;Pin the .NET SDK with &lt;code&gt;global.json&lt;/code&gt; and avoid &lt;code&gt;LangVersion latest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If XML documentation and warnings-as-errors are enabled, document every public API.&lt;/li&gt;
&lt;li&gt;Package the DLL, PDB, XML documentation, &lt;code&gt;.meta&lt;/code&gt;, license, and linker settings together.&lt;/li&gt;
&lt;li&gt;Control the Plugin Importer reference graph with asmdef files and &lt;code&gt;Override References&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Test the production IL2CPP backend, stripping level, and real platform instead of stopping at the Editor.&lt;/li&gt;
&lt;li&gt;Preserve debuggability with &lt;code&gt;InformationalVersion&lt;/code&gt;, commit hashes, Source Link, and synchronized symbols.&lt;/li&gt;
&lt;li&gt;Never place secrets in a managed DLL because it can be decompiled.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;External compilation gives you a newer &lt;strong&gt;C# compiler&lt;/strong&gt;, not a newer &lt;strong&gt;Unity .NET runtime&lt;/strong&gt;. Once that distinction is clear, you can bring newer syntax, normal .NET SDK tooling, analyzers, Source Generators, and CI practices into Unity library development without assuming that Unity's execution restrictions have disappeared.&lt;/p&gt;

&lt;p&gt;A practical package 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;Company.Product.Core.dll
  └ netstandard2.1, no Unity dependency,
     newer syntax used internally.

Company.Product.Unity.dll
  └ UnityEngine conversions, Components,
     and Unity-specific behavior.

Company.Product.Editor.dll
  └ Inspectors, importers, MenuItem commands,
     and editor-only tooling.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The benefits outweigh the operational cost when the full path is automated: external build, test, Unity import, IL2CPP Player build, device verification, and versioned artifact publication.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/plug-ins-managed.html" rel="noopener noreferrer"&gt;Unity Manual: Managed plug-ins&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/csharp-compiler.html" rel="noopener noreferrer"&gt;Unity Manual: C# compiler and language version reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/dotnet-profile-support.html" rel="noopener noreferrer"&gt;Unity Manual: API compatibility levels for .NET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/assembly-definitions-referencing.html" rel="noopener noreferrer"&gt;Unity Manual: Referencing assemblies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/scripting-restrictions.html" rel="noopener noreferrer"&gt;Unity Manual: IL2CPP limitations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/managed-code-stripping.html" rel="noopener noreferrer"&gt;Unity Manual: Managed code stripping&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/dotnet/csharp/language-reference/language-versioning" rel="noopener noreferrer"&gt;Microsoft Learn: C# language versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/dotnet/core/tools/global-json" rel="noopener noreferrer"&gt;Microsoft Learn: global.json overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/dotnet/standard/frameworks" rel="noopener noreferrer"&gt;Microsoft Learn: Target frameworks in SDK-style projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/dotnet/csharp/whats-new/tutorials/primary-constructors" rel="noopener noreferrer"&gt;Microsoft Learn: Primary constructors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/dotnet/csharp/language-reference/compiler-messages/cs1591" rel="noopener noreferrer"&gt;Microsoft Learn: Compiler Warning CS1591&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>dotnet</category>
      <category>il2cpp</category>
    </item>
    <item>
      <title>Why Use Precompiled C# DLLs in Unity? asmdef, Shared Server Code, and Practical Trade-offs</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Wed, 05 Aug 2026 07:39:29 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/why-use-precompiled-c-dlls-in-unity-asmdef-shared-server-code-and-practical-trade-offs-45p5</link>
      <guid>https://dev.to/gamedevtoollab/why-use-precompiled-c-dlls-in-unity-asmdef-shared-server-code-and-practical-trade-offs-45p5</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In a typical Unity project, C# source files live under &lt;code&gt;Assets&lt;/code&gt;, and the Unity Editor compiles them for you. For a small project, this is usually the simplest and most productive workflow.&lt;/p&gt;

&lt;p&gt;As a project grows, however, different requirements begin to appear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Share calculations and validation rules between a Unity client and a game server written in C#.&lt;/li&gt;
&lt;li&gt;Test game logic quickly without starting Unity.&lt;/li&gt;
&lt;li&gt;Remove mature, rarely changed infrastructure code from Unity's everyday recompilation loop.&lt;/li&gt;
&lt;li&gt;Use newer C# syntax than the Unity Editor can compile directly.&lt;/li&gt;
&lt;li&gt;Distribute the same library to multiple games.&lt;/li&gt;
&lt;li&gt;Deliver a versioned binary internally or externally instead of distributing source code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One possible answer is to compile selected C# code outside Unity and import the result as a managed DLL.&lt;/p&gt;

&lt;p&gt;Unity's documentation describes managed plug-ins as .NET assemblies produced outside Unity with tools such as Visual Studio, MSBuild, or the .NET SDK. Normal scripts in the Unity project are recompiled by Unity when their source changes, while an imported DLL is treated as a precompiled asset.&lt;/p&gt;

&lt;p&gt;This does &lt;strong&gt;not&lt;/strong&gt; mean that moving every C# file into a DLL automatically makes a project faster, safer, or easier to maintain. DLLs are most valuable for code that changes relatively infrequently, has little or no dependency on Unity APIs, and is reused in more than one runtime or product.&lt;/p&gt;

&lt;p&gt;This is Part 1 of a two-part series. It focuses on the benefits, architecture, and adoption criteria for external C# DLLs in Unity. In particular, it covers shared client/server code, compilation boundaries, standard .NET tests, dependency control, versioned distribution, and the trade-offs compared with Assembly Definition files.&lt;/p&gt;

&lt;p&gt;Part 2 covers the implementation details: newer C# syntax, &lt;code&gt;global.json&lt;/code&gt;, &lt;code&gt;csproj&lt;/code&gt;, &lt;code&gt;netstandard2.1&lt;/code&gt;, copying DLLs into Unity, Plugin Importer settings, &lt;code&gt;.meta&lt;/code&gt; files, IL2CPP/AOT restrictions, and debugging symbols.&lt;/p&gt;

&lt;p&gt;The examples are based on Unity 6.5 as of July 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Part Covers
&lt;/h2&gt;

&lt;p&gt;DLL-based workflows combine several different topics: architecture, compilation, language versions, Unity's Plugin Importer, IL2CPP, packaging, and deployment. Mixing all of them into one article makes it difficult to separate the adoption decision from the implementation details, so this guide is split into two parts.&lt;/p&gt;

&lt;p&gt;This first part covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The difference between an external DLL and an asmdef.&lt;/li&gt;
&lt;li&gt;Sharing code between a Unity client and a C# server.&lt;/li&gt;
&lt;li&gt;The effect on Unity compilation time.&lt;/li&gt;
&lt;li&gt;Running unit tests without launching Unity.&lt;/li&gt;
&lt;li&gt;Using project boundaries to keep Unity dependencies out of core logic.&lt;/li&gt;
&lt;li&gt;Designing a small public API.&lt;/li&gt;
&lt;li&gt;Distributing a versioned library across multiple projects.&lt;/li&gt;
&lt;li&gt;The disadvantages and operational costs of DLLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Main Recommendation: Put Stable Core Code in a DLL
&lt;/h2&gt;

&lt;p&gt;The best candidates for an external DLL are usually the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Damage, reward, stamina, and experience calculations.&lt;/li&gt;
&lt;li&gt;Input and request validation.&lt;/li&gt;
&lt;li&gt;IDs, enums, value objects, and protocol-level types.&lt;/li&gt;
&lt;li&gt;State transitions and game progression logic that do not depend on Unity APIs.&lt;/li&gt;
&lt;li&gt;General-purpose encryption, compression, transformation, and diff logic.&lt;/li&gt;
&lt;li&gt;SDKs used by multiple games, servers, live-ops tools, or batch processes.&lt;/li&gt;
&lt;li&gt;Mature infrastructure code that is no longer edited every day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following code is usually easier to keep as source inside the Unity project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gameplay code built around frequently edited &lt;code&gt;MonoBehaviour&lt;/code&gt; classes.&lt;/li&gt;
&lt;li&gt;Components tuned repeatedly through the Inspector.&lt;/li&gt;
&lt;li&gt;Logic tightly coupled to scenes, prefabs, animations, or Timeline.&lt;/li&gt;
&lt;li&gt;Code that changes several times a day while a feature is still being designed.&lt;/li&gt;
&lt;li&gt;Editor extensions that must track Unity API changes closely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical architecture is often a three-layer split rather than an attempt to move everything into a DLL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Game.Shared.Core
  └ Shared logic with no UnityEngine dependency.
     Built as an external DLL.

Game.Unity
  └ MonoBehaviour, ScriptableObject, UI, input, and presentation.
     Calls Shared.Core.

Game.Server
  └ ASP.NET Core, Worker Service, or another server runtime.
     References Shared.Core.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value does not come from changing a file extension to &lt;code&gt;.dll&lt;/code&gt;. It comes from establishing a Unity-independent core and reusing exactly the same rules across multiple execution environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  DLLs and asmdef Files Solve Related but Different Problems
&lt;/h2&gt;

&lt;p&gt;When Unity compilation time becomes a problem, the first tool to consider is usually an Assembly Definition file, or &lt;code&gt;.asmdef&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An asmdef divides source code inside the Unity project into multiple assemblies. Unity can then recompile the changed assembly and the assemblies that depend on it, rather than rebuilding one enormous &lt;code&gt;Assembly-CSharp.dll&lt;/code&gt; every time.&lt;/p&gt;

&lt;p&gt;An external DLL also creates an assembly boundary, but its development workflow is different.&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;asmdef&lt;/th&gt;
&lt;th&gt;External DLL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source location&lt;/td&gt;
&lt;td&gt;Inside the Unity project&lt;/td&gt;
&lt;td&gt;In an external .NET project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compiler&lt;/td&gt;
&lt;td&gt;Unity's bundled Roslyn&lt;/td&gt;
&lt;td&gt;A selected .NET SDK/Roslyn version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Applying changes&lt;/td&gt;
&lt;td&gt;Unity recompiles automatically&lt;/td&gt;
&lt;td&gt;Rebuild the DLL, then update it in Unity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Referencing Unity APIs&lt;/td&gt;
&lt;td&gt;Straightforward&lt;/td&gt;
&lt;td&gt;Requires references to Unity assemblies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Newer C# syntax&lt;/td&gt;
&lt;td&gt;Limited by Unity's support&lt;/td&gt;
&lt;td&gt;Limited by the selected compiler, runtime, and API compatibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharing with a server&lt;/td&gt;
&lt;td&gt;Possible, but requires extra structure&lt;/td&gt;
&lt;td&gt;Straightforward for a pure .NET library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debugging&lt;/td&gt;
&lt;td&gt;Same as normal Unity scripts&lt;/td&gt;
&lt;td&gt;Requires symbol and source-version management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frequently edited code&lt;/td&gt;
&lt;td&gt;Well suited&lt;/td&gt;
&lt;td&gt;Additional build/copy steps become costly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binary distribution&lt;/td&gt;
&lt;td&gt;Not its main purpose&lt;/td&gt;
&lt;td&gt;Well suited&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If compilation time is the only concern, start with asmdef files. They usually provide most of the benefit with less operational overhead.&lt;/p&gt;

&lt;p&gt;External DLLs become more attractive when you also need one or more of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A shared implementation for client, server, and tools.&lt;/li&gt;
&lt;li&gt;A separately maintained build pipeline.&lt;/li&gt;
&lt;li&gt;A compiler newer than the one Unity uses for project source.&lt;/li&gt;
&lt;li&gt;Versioned binary distribution.&lt;/li&gt;
&lt;li&gt;Strict separation from Unity APIs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Updating a DLL still causes Unity assemblies that reference it to be recompiled. DLLs do not eliminate Unity compilation entirely. They are effective when a large, stable body of code is precompiled and Unity sees only a relatively small, stable public API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefit 1: Share the Same Rules Between Unity and a C# Server
&lt;/h2&gt;

&lt;p&gt;The most obvious benefit is sharing code with .NET applications outside Unity.&lt;/p&gt;

&lt;p&gt;Suppose both the client and server contain this calculation:&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;int&lt;/span&gt; &lt;span class="nf"&gt;CalculateRequiredGold&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;currentLevel&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="m"&gt;100&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;currentLevel&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;currentLevel&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copying it into both projects works initially, but sooner or later only one copy changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The server changes the coefficient after level 20.&lt;/li&gt;
&lt;li&gt;The Unity client's prediction still uses the old formula.&lt;/li&gt;
&lt;li&gt;The UI shows that an upgrade costs 1,000 gold.&lt;/li&gt;
&lt;li&gt;The server API rejects the request because the real cost is 1,200 gold.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This type of inconsistency can be harder to discover than a networking failure. The API is returning a valid error, and the client may not throw an exception. The two sides simply disagree about the rules.&lt;/p&gt;

&lt;p&gt;A shared library lets the Unity client, the server, internal tools, and tests reference the same implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shared.GameRules.dll
├── LevelRule
├── DamageFormula
├── RewardCalculator
├── ItemUseValidator
└── ProtocolConstants
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server itself can still target a modern runtime such as &lt;code&gt;.NET 10&lt;/code&gt;. Only the shared library needs to target a compatibility surface such as &lt;code&gt;netstandard2.1&lt;/code&gt;, allowing both Unity and the server to reference it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code That Is Easy to Share
&lt;/h3&gt;

&lt;p&gt;The best shared code has explicit inputs and outputs and does not access global engine 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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;BattleParameter&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;BattleParameter&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;skillPower&lt;/span&gt;&lt;span class="p"&gt;)&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;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;defense&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;SkillPower&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;skillPower&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;Attack&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="k"&gt;public&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="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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;SkillPower&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;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DamageFormula&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;int&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;BattleParameter&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&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;Attack&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;SkillPower&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;100L&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;Defense&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;raw&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="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&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;return&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&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;MaxValue&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="n"&gt;MaxValue&lt;/span&gt;
            &lt;span class="p"&gt;:&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="n"&gt;raw&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;Using ordinary classes and &lt;code&gt;readonly struct&lt;/code&gt; values at the public boundary tends to reduce friction with Unity's compiler and serializers. The important point is not the exact syntax. The important point is that the calculation does not depend on &lt;code&gt;UnityEngine.Object&lt;/code&gt;, &lt;code&gt;GameObject&lt;/code&gt;, &lt;code&gt;Transform&lt;/code&gt;, &lt;code&gt;Time&lt;/code&gt;, or any other engine-owned state.&lt;/p&gt;

&lt;p&gt;The calculation also widens intermediate multiplication to &lt;code&gt;long&lt;/code&gt;. The final value saturates to the range from 1 to &lt;code&gt;int.MaxValue&lt;/code&gt;. In production code, the overflow policy should be part of the game specification: reject the input with &lt;code&gt;checked&lt;/code&gt;, saturate, wrap intentionally, or use a larger numeric type.&lt;/p&gt;

&lt;p&gt;Once the policy is implemented in the shared library, the Unity client, server, and tests all observe the same behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code That Should Not Be Shared
&lt;/h3&gt;

&lt;p&gt;Using C# on both sides does not mean that every implementation belongs in a common DLL.&lt;/p&gt;

&lt;p&gt;The following concerns are usually safer as server-only code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hidden loot probabilities and anti-cheat rules.&lt;/li&gt;
&lt;li&gt;Database transactions.&lt;/li&gt;
&lt;li&gt;Authentication, authorization, and signature verification.&lt;/li&gt;
&lt;li&gt;Server-side inventory and billing decisions.&lt;/li&gt;
&lt;li&gt;Secret keys and server-only configuration.&lt;/li&gt;
&lt;li&gt;Logic whose authoritative input is the server clock or server-owned state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following concerns are usually Unity-specific:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frame updates.&lt;/li&gt;
&lt;li&gt;Object lifetime and scene ownership.&lt;/li&gt;
&lt;li&gt;Presentation state.&lt;/li&gt;
&lt;li&gt;Animation and VFX timing.&lt;/li&gt;
&lt;li&gt;Inspector-driven authoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A shared &lt;code&gt;ItemUseValidator&lt;/code&gt; on the client can reject an obviously invalid input before sending a request. This improves feedback and reduces unnecessary traffic. The server must still execute the authoritative validation again.&lt;/p&gt;

&lt;p&gt;A client-side managed DLL can be decompiled. Never place secret values or security-sensitive server decisions in a shared client DLL.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Same DLL Does Not Guarantee the Same Result
&lt;/h3&gt;

&lt;p&gt;Sharing the implementation is only one part of deterministic behavior. Different inputs or execution assumptions still produce different results.&lt;/p&gt;

&lt;p&gt;For client/server rules, define at least the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The rounding mode and when rounding occurs.&lt;/li&gt;
&lt;li&gt;Whether a value uses &lt;code&gt;float&lt;/code&gt;, &lt;code&gt;double&lt;/code&gt;, &lt;code&gt;decimal&lt;/code&gt;, or an integer/fixed-point representation.&lt;/li&gt;
&lt;li&gt;Whether time is UTC and which clock is authoritative.&lt;/li&gt;
&lt;li&gt;Whether only a random seed is shared or the random algorithm is also fixed.&lt;/li&gt;
&lt;li&gt;Whether parsing and formatting may depend on &lt;code&gt;CultureInfo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Whether overflow throws, saturates, or wraps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For currencies and rewards that must match exactly, storing the smallest unit as an integer is usually safer than using &lt;code&gt;float&lt;/code&gt;. If decimal values are required, the &lt;code&gt;Math.Round&lt;/code&gt; mode and the order of operations should also be specified.&lt;/p&gt;

&lt;p&gt;Do not treat the device clock as authoritative. A client may use the shared library to display an estimate, but the server should recalculate the final result from server-managed UTC time.&lt;/p&gt;

&lt;p&gt;Randomness has the same issue. &lt;code&gt;UnityEngine.Random&lt;/code&gt; on the client and &lt;code&gt;System.Random&lt;/code&gt; on the server are not guaranteed to produce the same sequence from the same seed. If results must match, either pass the random value into the rule, fix the algorithm inside the shared library, or let the server produce and return the final result.&lt;/p&gt;

&lt;p&gt;A DLL gives you one implementation of the rule. Production consistency additionally requires the same inputs, rounding, time base, and random policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharing DTOs Requires Versioning Discipline
&lt;/h3&gt;

&lt;p&gt;Sharing request and response DTOs can reduce mismatched names and types. It can also couple the release cadence of the client and server too tightly.&lt;/p&gt;

&lt;p&gt;Long-running products should decide how contract evolution works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use separate DTOs per API version.&lt;/li&gt;
&lt;li&gt;Treat additions as compatible and removals or type changes as breaking changes.&lt;/li&gt;
&lt;li&gt;Make a schema such as Protobuf the source of truth and generate code for both sides.&lt;/li&gt;
&lt;li&gt;Share domain values in the common DLL, but map them to HTTP DTOs separately in the client and server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"We can share the type" is not the same as "we should use this exact type forever." The boundary should match the deployment and compatibility model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefit 2: Reduce the Source Unity Must Compile in the Daily Loop
&lt;/h2&gt;

&lt;p&gt;Unity compiles normal C# scripts when they change, while imported managed DLLs are treated as precompiled assemblies.&lt;/p&gt;

&lt;p&gt;Consider a project with the following layout:&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
├── GamePlay          800 files
├── UI                500 files
├── Network           300 files
├── Data              400 files
└── StableFramework  1200 files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;StableFramework&lt;/code&gt; is mature and changes only once every few weeks, recompiling and reanalyzing all of its source during ordinary gameplay work may not provide much value.&lt;/p&gt;

&lt;p&gt;Moving it to an external DLL means that Unity normally handles only its public API and compiled IL. Changes in the game project do not rebuild the library itself.&lt;/p&gt;

&lt;p&gt;There are three configurations worth comparing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put everything in &lt;code&gt;Assembly-CSharp&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Split Unity source into multiple assemblies with asmdef files.&lt;/li&gt;
&lt;li&gt;Move stable code into an external DLL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For many projects, moving from the first configuration to the second produces most of the improvement. The third adds value when the library can genuinely operate on a release cycle independent of the Unity project.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Time Can Be Reduced
&lt;/h3&gt;

&lt;p&gt;An external DLL can reduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unity compilation of the moved source set.&lt;/li&gt;
&lt;li&gt;Unity-side Roslyn Analyzer execution for that source.&lt;/li&gt;
&lt;li&gt;Source enumeration during Unity project generation.&lt;/li&gt;
&lt;li&gt;IDE analysis of a very large Unity-generated project.&lt;/li&gt;
&lt;li&gt;Recompiling the same library independently in several Unity games.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following costs remain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building the external .NET project.&lt;/li&gt;
&lt;li&gt;Copying and importing the DLL into Unity.&lt;/li&gt;
&lt;li&gt;Recompiling Unity assemblies that depend on the updated DLL.&lt;/li&gt;
&lt;li&gt;Assembly reload and static initialization.&lt;/li&gt;
&lt;li&gt;Converting the managed DLL during an IL2CPP Player build.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Moving code that changes every few minutes into a DLL can make iteration slower because it adds &lt;code&gt;dotnet build&lt;/code&gt;, copy, and Unity import steps.&lt;/p&gt;

&lt;p&gt;The optimization is not "remove compilation." It is "remove stable code from the ordinary Unity edit-and-play loop."&lt;/p&gt;

&lt;h3&gt;
  
  
  Limit the Reference Graph or the Benefit Shrinks
&lt;/h3&gt;

&lt;p&gt;By default, a custom asmdef can implicitly reference precompiled DLLs whose Plugin Importer setting &lt;code&gt;Auto Referenced&lt;/code&gt; is enabled. Updating such a DLL may therefore recompile assemblies that do not actually use it.&lt;/p&gt;

&lt;p&gt;To reduce the affected graph, disable &lt;code&gt;Auto Referenced&lt;/code&gt; for the DLL and enable &lt;code&gt;Override References&lt;/code&gt; only on asmdef files that need it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shared.GameRules.dll
        ↑
Game.Domain.asmdef
        ↑
Game.Presentation.asmdef
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not implicitly reference the library from unrelated UI, editor, or test assemblies.&lt;/p&gt;

&lt;p&gt;The DLL boundary becomes valuable only when the reference graph is also kept small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Misconception: A DLL Does Not Automatically Make Runtime Code Faster
&lt;/h2&gt;

&lt;p&gt;"Precompiled DLL" can sound as if it executes faster than C# source stored in the Unity project. In practice, the main benefit is the development-time build boundary and reusability, not game runtime speed.&lt;/p&gt;

&lt;p&gt;Unity project source is eventually compiled into managed assemblies as well. With IL2CPP, Unity performs managed stripping across project assemblies and external DLLs, converts IL to C++, and produces native code. If the logic is the same, its origin as project source or a prebuilt DLL does not by itself create a meaningful runtime performance difference.&lt;/p&gt;

&lt;p&gt;Overengineering the boundary can even add costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allocations when copying data into public DTOs.&lt;/li&gt;
&lt;li&gt;Conversion between Unity-specific and shared types.&lt;/li&gt;
&lt;li&gt;Extra virtual calls introduced only for abstraction.&lt;/li&gt;
&lt;li&gt;Copies across DLL boundaries that exist only because of packaging.&lt;/li&gt;
&lt;li&gt;Reflection-based registration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These costs are not automatically serious, but they are a reason not to treat DLL packaging as a runtime optimization.&lt;/p&gt;

&lt;p&gt;For runtime performance, use the Unity Profiler and optimize the actual bottleneck: allocations, algorithms, data layout, Burst, Jobs, rendering, loading, or networking.&lt;/p&gt;

&lt;p&gt;The "speed" a DLL most commonly improves is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The time from editing code to returning to Play Mode.&lt;/li&gt;
&lt;li&gt;The speed of pure .NET tests that do not launch Unity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping these two definitions separate makes the result easier to measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefit 3: Run Unit Tests Without Starting Unity
&lt;/h2&gt;

&lt;p&gt;A Unity-independent DLL can be referenced by a normal .NET test project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── Shared.GameRules/
│   └── Shared.GameRules.csproj
└── Shared.GameRules.Tests/
    └── Shared.GameRules.Tests.csproj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test project can use xUnit, NUnit, MSTest, or any other compatible framework.&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;DamageFormulaTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Theory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineData&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;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;130&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineData&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="m"&gt;999&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Calculate_ReturnsExpectedValue&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;skillPower&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;parameter&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;BattleParameter&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;skillPower&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;DamageFormula&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;parameter&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;Equal&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;actual&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 enables a different development loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run tests in seconds without opening the Unity Editor.&lt;/li&gt;
&lt;li&gt;Use a lightweight Linux CI runner.&lt;/li&gt;
&lt;li&gt;Run the library in the same pipeline as server tests.&lt;/li&gt;
&lt;li&gt;Avoid Unity licenses and &lt;code&gt;Library&lt;/code&gt; cache setup for pure logic tests.&lt;/li&gt;
&lt;li&gt;Use the IDE's standard test runner.&lt;/li&gt;
&lt;li&gt;Add coverage, mutation testing, and benchmarks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unity Test Framework can also run EditMode tests. The point is not that Unity tests are bad; it is that Unity-independent logic does not need to pay the cost of Unity import, compilation, and the Unity test runner.&lt;/p&gt;

&lt;p&gt;Fast tests make it practical to cover more edge cases: zero, negative values, maximum values, overflow, rounding, level caps, time boundaries, and invalid inputs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep Unity Integration Tests in Unity
&lt;/h3&gt;

&lt;p&gt;External .NET tests cannot detect every problem. They do not validate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;MonoBehaviour&lt;/code&gt; lifecycle behavior.&lt;/li&gt;
&lt;li&gt;Unity serialization.&lt;/li&gt;
&lt;li&gt;Mapping to &lt;code&gt;ScriptableObject&lt;/code&gt; data.&lt;/li&gt;
&lt;li&gt;IL2CPP stripping.&lt;/li&gt;
&lt;li&gt;Main-thread restrictions.&lt;/li&gt;
&lt;li&gt;Platform-specific file or network behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use a two-level strategy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Normal .NET tests
  └ Fast, broad verification of shared logic.

Unity EditMode / PlayMode / device tests
  └ Unity integration, AOT, serialization, and platform behavior.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DLLs do not eliminate Unity tests. They let pure logic run in a more appropriate environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefit 4: Enforce Dependency Direction at Compile Time
&lt;/h2&gt;

&lt;p&gt;When all source is inside one Unity project, developers can often reference engine state simply because it is available.&lt;/p&gt;

&lt;p&gt;Examples include calling a persistence service directly from UI code or using &lt;code&gt;GameObject.Find&lt;/code&gt; inside domain logic.&lt;/p&gt;

&lt;p&gt;A separate &lt;code&gt;netstandard2.1&lt;/code&gt; project does not reference &lt;code&gt;UnityEngine&lt;/code&gt; by default:&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;// Does not compile in Shared.GameRules.&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="n"&gt;GameObject&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="s"&gt;"Player"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The limitation becomes architectural protection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domain logic cannot search scenes.&lt;/li&gt;
&lt;li&gt;Time is passed in or provided through a clock abstraction rather than read from &lt;code&gt;DateTime.Now&lt;/code&gt; everywhere.&lt;/li&gt;
&lt;li&gt;Randomness is abstracted instead of hardcoded to &lt;code&gt;UnityEngine.Random&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Logging does not depend directly on &lt;code&gt;Debug.Log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Core file operations do not hardcode &lt;code&gt;Application.persistentDataPath&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, a time-dependent rule can receive a clock:&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;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;EnergyRecoveryService&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;EnergyRecoveryService&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;int&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;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;lastRecoveredAt&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;recoverySeconds&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;recoverySeconds&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentOutOfRangeException&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;recoverySeconds&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;recoverySeconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"Recovery seconds must be greater than zero."&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;elapsed&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;UtcNow&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lastRecoveredAt&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;elapsed&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&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="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&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;elapsedSeconds&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;Ticks&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;TicksPerSecond&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;recoveredAmount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;elapsedSeconds&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;recoverySeconds&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;recoveredAmount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&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;MaxValue&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="n"&gt;MaxValue&lt;/span&gt;
            &lt;span class="p"&gt;:&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="n"&gt;recoveredAmount&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;Unity can provide a Unity-facing implementation, the server can provide a server implementation, and tests can provide a fixed clock. Configuration such as &lt;code&gt;recoverySeconds&lt;/code&gt; is validated at the boundary, preventing zero division and invalid behavior from entering the shared rule.&lt;/p&gt;

&lt;p&gt;The calculation uses integer ticks instead of going through &lt;code&gt;TimeSpan.TotalSeconds&lt;/code&gt; as a &lt;code&gt;double&lt;/code&gt;. Negative elapsed time becomes zero, and very large values saturate to &lt;code&gt;int.MaxValue&lt;/code&gt;. This makes the rounding and upper-bound policy explicit and easier to keep identical on the client and server.&lt;/p&gt;

&lt;p&gt;You can create the same architecture with asmdef files and strict conventions, but a separate project makes a Unity dependency a compilation error rather than a code-review preference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the Public API Small
&lt;/h2&gt;

&lt;p&gt;Every public type in a DLL becomes a contract for Unity, the server, tests, and tools. If every internal class is public, consumers will depend on implementation details that become difficult to change.&lt;/p&gt;

&lt;p&gt;Expose inputs and final results rather than every intermediate 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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;DamageResult&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;DamageResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="k"&gt;value&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;isCritical&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;IsCritical&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;isCritical&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;Value&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsCritical&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IDamageCalculator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;DamageResult&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;BattleParameter&lt;/span&gt; &lt;span class="n"&gt;parameter&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;randomValue&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;Passing a random value into the calculation makes the rule deterministic for a given input. If exact client/server agreement is required, specify whether you share the random value, a seed plus a fixed algorithm, or only the server-produced result.&lt;/p&gt;

&lt;p&gt;Public API guidelines that help preserve compatibility include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not expose UnityEngine types from the Core DLL.&lt;/li&gt;
&lt;li&gt;Choose arrays or &lt;code&gt;IReadOnlyList&amp;lt;T&amp;gt;&lt;/code&gt; based on the caller's real needs.&lt;/li&gt;
&lt;li&gt;Avoid unrestricted setters.&lt;/li&gt;
&lt;li&gt;Assign explicit numeric values to enums used in protocols or persisted data.&lt;/li&gt;
&lt;li&gt;Treat optional parameter defaults as part of the contract.&lt;/li&gt;
&lt;li&gt;Remember that renaming a public parameter can affect callers using named arguments.&lt;/li&gt;
&lt;li&gt;Do not use exceptions as an ordinary branch result.&lt;/li&gt;
&lt;li&gt;Default to &lt;code&gt;internal&lt;/code&gt; and make only intentional entry points public.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The library's value is not the number of classes it contains. It is the stability of the boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Split DLLs by Reasons to Change
&lt;/h3&gt;

&lt;p&gt;Dividing DLLs by file count usually causes all of them to be released together anyway. A better boundary reflects who changes the code, why it changes, and how often it changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Company.Protocol
  └ Network contracts.
     Updated with client/server agreement.

Company.GameRules
  └ Calculations and validation.
     Updated with game-design rules.

Company.Foundation
  └ Logging, IDs, and shared data structures.
     Updated infrequently by a platform team.

Company.UnityAdapter
  └ Unity API integration.
     Updated for engine and presentation changes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If protocol contracts and Unity adapters live in the same DLL, a Unity-only update can unnecessarily force a server-side package update. On the other hand, splitting too aggressively increases the number of packages, references, and version combinations.&lt;/p&gt;

&lt;p&gt;A useful rule is to split code that needs a separate release reason, has different consumers, or needs a one-way dependency boundary. A namespace difference alone does not justify another DLL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefit 5: Distribute the Same Version to Multiple Projects
&lt;/h2&gt;

&lt;p&gt;Copying source into each Unity repository makes it difficult to know which project contains which revision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ProjectA/Assets/Common/...
ProjectB/Assets/Common/...
ProjectC/Assets/Common/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A built and versioned DLL makes the dependency explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shared.GameRules 2.3.1
Shared.Networking 4.0.0
Company.Logging 1.8.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Possible distribution methods include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Commit the DLL directly to the consumer repository.&lt;/li&gt;
&lt;li&gt;Put the DLL in a Unity Package.&lt;/li&gt;
&lt;li&gt;Publish a UPM package through a scoped registry.&lt;/li&gt;
&lt;li&gt;Publish the .NET package to an internal NuGet feed and copy its DLL into a generated Unity package.&lt;/li&gt;
&lt;li&gt;Synchronize a CI artifact into the Unity project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For companies maintaining several games, knowing the exact version in use is often more important than always running the latest version.&lt;/p&gt;

&lt;p&gt;Automatic updates can break all projects at once. Pin each consumer to a known version, test the upgrade, and then update deliberately.&lt;/p&gt;

&lt;h3&gt;
  
  
  DLLs Fit Semantic Versioning Well
&lt;/h3&gt;

&lt;p&gt;A public DLL API maps naturally to Semantic Versioning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Patch&lt;/strong&gt;: compatible bug fixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minor&lt;/strong&gt;: backward-compatible additions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Major&lt;/strong&gt;: removed or renamed types, changed parameters, and other breaking changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deleting or renaming an assembly, moving a public type, or changing public method signatures should be treated as a breaking change.&lt;/p&gt;

&lt;p&gt;A physical binary boundary encourages teams to distinguish between freely changeable internal implementation and public compatibility commitments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefit 6: Deliver Functionality Without Shipping Source
&lt;/h2&gt;

&lt;p&gt;A Unity Asset Store tool, external SDK, or partner library may need to be distributed as a DLL instead of source code.&lt;/p&gt;

&lt;p&gt;Unity's documentation lists supplying code without source as one use case for managed plug-ins.&lt;/p&gt;

&lt;p&gt;Benefits include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consumers are less likely to modify internal implementation accidentally.&lt;/li&gt;
&lt;li&gt;The deliverable can be smaller and more controlled.&lt;/li&gt;
&lt;li&gt;The public API becomes an explicit product surface.&lt;/li&gt;
&lt;li&gt;Implementation details are less visible during ordinary use.&lt;/li&gt;
&lt;li&gt;The same binary can be delivered to multiple consumers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, managed DLLs can be decompiled. Obfuscation can increase the cost of analysis, but it does not make the code secret.&lt;/p&gt;

&lt;p&gt;DLL packaging is not a substitute for security:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not embed API keys.&lt;/li&gt;
&lt;li&gt;Do not ship server private keys.&lt;/li&gt;
&lt;li&gt;Do not put the final billing, reward, or entitlement decision in the client.&lt;/li&gt;
&lt;li&gt;Do not rely on the DLL format for copy protection.&lt;/li&gt;
&lt;li&gt;Keep license terms and technical enforcement as separate concerns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A DLL is useful for preventing casual edits and keeping implementation out of the normal source view. It cannot guarantee that nobody will inspect the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should the DLL Reference UnityEngine?
&lt;/h2&gt;

&lt;p&gt;A Unity-specific SDK may legitimately need to call Unity APIs from inside a DLL.&lt;/p&gt;

&lt;p&gt;Technically, you can build such an assembly by referencing Unity modules such as &lt;code&gt;UnityEngine.CoreModule.dll&lt;/code&gt;. For code shared with a server, however, this creates an unnecessary dependency.&lt;/p&gt;

&lt;p&gt;A better package layout is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Company.Product.Core.dll
  └ netstandard2.1, no Unity dependency.

Company.Product.Unity.dll
  └ UnityEngine-dependent adapters.

Company.Product.Editor.dll
  └ UnityEditor-dependent editor tooling.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Core&lt;/code&gt; can be used by servers, CLIs, and tests. &lt;code&gt;Unity&lt;/code&gt; converts between Unity types and the core model and contains Components. &lt;code&gt;Editor&lt;/code&gt; contains importers, inspectors, and menu commands.&lt;/p&gt;

&lt;p&gt;If you do build a Unity-dependent DLL, manage at least the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Unity version whose assemblies are referenced.&lt;/li&gt;
&lt;li&gt;Separation of runtime and editor assemblies.&lt;/li&gt;
&lt;li&gt;Plugin Importer platform settings.&lt;/li&gt;
&lt;li&gt;Rebuilding when Unity APIs change.&lt;/li&gt;
&lt;li&gt;Preventing &lt;code&gt;UnityEditor&lt;/code&gt; references from entering Player builds.&lt;/li&gt;
&lt;li&gt;Changes that Unity's API Updater cannot rewrite inside the precompiled binary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The thinner the Unity-specific layer is, the easier it is to support multiple Unity LTS versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disadvantages of External DLLs
&lt;/h2&gt;

&lt;p&gt;The benefits come with operational costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  More Steps Before a Change Appears in Unity
&lt;/h3&gt;

&lt;p&gt;A source file in Unity is recompiled after saving. An external DLL must be built and copied or packaged.&lt;/p&gt;

&lt;p&gt;File watching, IDE tasks, automatic builds, and CI artifacts can reduce the friction. The most effective solution is still to avoid placing frequently edited code in the DLL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Weaker Inspector and Source Navigation Workflows
&lt;/h3&gt;

&lt;p&gt;A DLL can contain &lt;code&gt;MonoBehaviour&lt;/code&gt; types, but the source is not a normal script asset in the project. Navigation, script assets, serialization compatibility, and class renames require more care.&lt;/p&gt;

&lt;p&gt;Game-specific Components are usually easier to maintain in Unity source, with the DLL focused on logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transitive Dependencies Must Be Shipped
&lt;/h3&gt;

&lt;p&gt;If the .NET project references a NuGet package, Unity also needs the compatible dependent assemblies. Unity does not resolve &lt;code&gt;.deps.json&lt;/code&gt; in the same way as a normal .NET application.&lt;/p&gt;

&lt;p&gt;Different Unity packages may also ship conflicting versions of the same assembly.&lt;/p&gt;

&lt;p&gt;Prefer few external dependencies. When one is necessary, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;netstandard2.1&lt;/code&gt; asset exists.&lt;/li&gt;
&lt;li&gt;IL2CPP support is documented.&lt;/li&gt;
&lt;li&gt;Native binaries are not required, or are available for every target.&lt;/li&gt;
&lt;li&gt;Transitive dependencies are known and packaged.&lt;/li&gt;
&lt;li&gt;Same-name assembly conflicts can be managed.&lt;/li&gt;
&lt;li&gt;Redistribution is permitted by the license.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Public API Changes Become More Expensive
&lt;/h3&gt;

&lt;p&gt;Internal implementation remains flexible, but a public API change can affect Unity, the server, and every tool at once.&lt;/p&gt;

&lt;p&gt;This is a cost, but it also creates useful pressure to keep dependencies clean. Avoid a single enormous interface. Prefer small types and purpose-specific services.&lt;/p&gt;

&lt;h3&gt;
  
  
  DLLs Are Not a Security Boundary
&lt;/h3&gt;

&lt;p&gt;Managed assemblies remain inspectable. DLLs are a packaging and architecture choice, not a secret-storage mechanism.&lt;/p&gt;

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

&lt;p&gt;A large Unity project does not need to move thousands of files at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Find Unity-Independent Code
&lt;/h3&gt;

&lt;p&gt;Good candidates usually have these properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;using UnityEngine;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Static or input/output-oriented calculations.&lt;/li&gt;
&lt;li&gt;DTOs and value objects.&lt;/li&gt;
&lt;li&gt;Validation rules.&lt;/li&gt;
&lt;li&gt;String, time, and numeric conversion.&lt;/li&gt;
&lt;li&gt;Pure data processing before or after networking.&lt;/li&gt;
&lt;li&gt;Testable without scenes or editor state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Create a Boundary with asmdef First
&lt;/h3&gt;

&lt;p&gt;Before moving code to another repository or project, create something such as &lt;code&gt;Game.Domain.asmdef&lt;/code&gt; inside Unity and correct the dependency direction.&lt;/p&gt;

&lt;p&gt;If the assembly still references large parts of Unity, the architecture needs separation before it needs packaging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Move the Assembly to an External .NET Project
&lt;/h3&gt;

&lt;p&gt;Replace Unity-specific parameters with standard or domain-specific values where appropriate:&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 in a cross-runtime core library.&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="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="c1"&gt;// Easier to share.&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="n"&gt;Position3&lt;/span&gt; &lt;span class="n"&gt;position&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 create custom replacements for every Unity type without a real reason. If vector math does not need to be shared, leaving it in the Unity layer may be the better choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Add Normal .NET Tests
&lt;/h3&gt;

&lt;p&gt;Before and after the move, create characterization tests that prove behavior remains the same. Lock down calculations, rounding, caps, random input, and date boundaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Import the DLL into Unity
&lt;/h3&gt;

&lt;p&gt;Part 2 covers &lt;code&gt;global.json&lt;/code&gt;, &lt;code&gt;csproj&lt;/code&gt;, Plugin Importer, and &lt;code&gt;.meta&lt;/code&gt; management.&lt;/p&gt;

&lt;p&gt;It is acceptable to begin with &lt;code&gt;Auto Referenced&lt;/code&gt; enabled. After the integration works, restrict the reference graph with asmdef files and &lt;code&gt;Override References&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Verify IL2CPP
&lt;/h3&gt;

&lt;p&gt;Pay special attention to reflection, serialization, generics, async code, and threading. Editor success is not the final validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Automate Distribution and Version Updates
&lt;/h3&gt;

&lt;p&gt;Replace manual copying with a pipeline that produces the DLL, PDB, XML documentation, license, and linker configuration as one artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Goal or situation&lt;/th&gt;
&lt;th&gt;Recommended first choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Improve Unity compilation time only&lt;/td&gt;
&lt;td&gt;Start with asmdef&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove mature, large code from the daily edit loop&lt;/td&gt;
&lt;td&gt;Consider an external DLL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share calculations between Unity and a C# server&lt;/td&gt;
&lt;td&gt;A Unity-independent DLL is a strong option&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share code with a CLI or data-processing tool&lt;/td&gt;
&lt;td&gt;A DLL is a strong option&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edit &lt;code&gt;MonoBehaviour&lt;/code&gt; classes every day&lt;/td&gt;
&lt;td&gt;Unity source plus asmdef&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test Unity-independent logic quickly&lt;/td&gt;
&lt;td&gt;External DLL or independent .NET project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Make code impossible to inspect&lt;/td&gt;
&lt;td&gt;DLLs do not solve this&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distribute one foundation library to several games&lt;/td&gt;
&lt;td&gt;DLL plus UPM package&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The central criteria are not file count. They are &lt;strong&gt;change frequency and number of consumers&lt;/strong&gt;. Stable code used outside Unity and tested independently gains the most from an external DLL.&lt;/p&gt;

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

&lt;p&gt;The practical benefits of turning selected Unity code into an external managed DLL are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Share calculations and validation between a Unity client and a C# server.&lt;/li&gt;
&lt;li&gt;Remove mature code from Unity's normal source recompilation loop.&lt;/li&gt;
&lt;li&gt;Run fast, standard .NET tests without starting Unity.&lt;/li&gt;
&lt;li&gt;Prevent Unity dependencies through compile-time project boundaries.&lt;/li&gt;
&lt;li&gt;Distribute a versioned artifact to multiple games.&lt;/li&gt;
&lt;li&gt;Treat the public API as an explicit compatibility contract.&lt;/li&gt;
&lt;li&gt;Use asmdef first when compilation time is the only goal.&lt;/li&gt;
&lt;li&gt;Do not expect packaging alone to improve runtime performance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most valuable combination is usually the ability to use the same game rules on the client and server and to test those rules extensively without Unity.&lt;/p&gt;

&lt;p&gt;At the same time, sharing one DLL is not enough to guarantee identical behavior. Inputs, rounding, time, randomness, culture, and overflow policies must also be aligned, while the server remains authoritative.&lt;/p&gt;

&lt;p&gt;A useful final architecture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stable pure logic
  └ Unity-independent external DLL.

Frequently changing gameplay code
  └ Unity source divided with asmdef files.

Unity integration
  └ Thin MonoBehaviour and ScriptableObject adapters.

Server-specific behavior
  └ Modern .NET server projects.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Part 2 shows how to implement this structure with a fixed .NET SDK, a &lt;code&gt;netstandard2.1&lt;/code&gt; project, newer C# syntax inside the DLL, Plugin Importer settings, IL2CPP/AOT testing, reproducible artifacts, and debugging symbols.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/plug-ins-managed.html" rel="noopener noreferrer"&gt;Unity Manual: Managed plug-ins&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/csharp-compiler.html" rel="noopener noreferrer"&gt;Unity Manual: C# compiler and language version reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/assembly-definitions-intro.html" rel="noopener noreferrer"&gt;Unity Manual: Introduction to assemblies in Unity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/assembly-definitions-referencing.html" rel="noopener noreferrer"&gt;Unity Manual: Referencing assemblies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/dotnet-profile-support.html" rel="noopener noreferrer"&gt;Unity Manual: API compatibility levels for .NET&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>dotnet</category>
      <category>gamedev</category>
    </item>
    <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>
  </channel>
</rss>
