<?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>Unity 6.3 Memory Management: When to Use GC, Destroy, and Dispose</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Fri, 02 Oct 2026 05:34:32 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/unity-63-memory-management-when-to-use-gc-destroy-and-dispose-4dec</link>
      <guid>https://dev.to/gamedevtoollab/unity-63-memory-management-when-to-use-gc-destroy-and-dispose-4dec</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Should you assign &lt;code&gt;null&lt;/code&gt;, call &lt;code&gt;Destroy&lt;/code&gt;, or use &lt;code&gt;Dispose&lt;/code&gt;? What about &lt;code&gt;Resources.UnloadUnusedAssets&lt;/code&gt; and &lt;code&gt;GC.Collect&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;The confusion starts when &lt;strong&gt;dropping a C# reference, destroying a Unity object, and releasing an acquired asset reference&lt;/strong&gt; are treated as the same operation. Unity uses several kinds of memory; garbage collection does not manage them all.&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;This guide starts with how a resource was acquired, who owns it, and how long it remains in use. The goal is to prevent both leaks and premature cleanup of resources that someone still needs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Scope and validation:&lt;/strong&gt; This article targets &lt;strong&gt;Unity 6.3 LTS (6000.3)&lt;/strong&gt; and conventional GameObject/MonoBehaviour development. The Japanese source records a documentation review dated &lt;strong&gt;September 15, 2026&lt;/strong&gt;, using Addressables 2.10, Collections 2.6, and Memory Profiler 1.1 documentation. Package versions are separate from the Editor version. Entities-specific lifecycles and native plugin internals are outside this article's scope.&lt;/p&gt;

&lt;p&gt;An independent review of the Japanese version reported successful compilation of the four complete C# examples in &lt;strong&gt;Unity 6000.3.22f1, Addressables 2.10.3, and Collections 2.6.3&lt;/strong&gt;. That report is not a runtime test. This English edition has not been recompiled, run in a Player, or measured on a target device; comments and diagnostic messages have been translated.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Choose the cleanup method by acquisition, not just type
&lt;/h2&gt;

&lt;p&gt;Unity distinguishes managed memory, C# unmanaged memory, and native memory. Allocations accessed through &lt;code&gt;NativeArray&lt;/code&gt; and similar APIs may appear under native memory in the Profiler. &lt;strong&gt;C# garbage collection cannot reclaim every kind of Unity or GPU resource.&lt;/strong&gt; IL2CPP still uses garbage collection for managed memory.&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;How the resource was acquired&lt;/th&gt;
&lt;th&gt;Normal cleanup&lt;/th&gt;
&lt;th&gt;Common mistake&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ordinary C# object, array, or &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Remove unnecessary references; let GC reclaim it&lt;/td&gt;
&lt;td&gt;Expecting &lt;code&gt;null&lt;/code&gt; to free memory immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GameObject from &lt;code&gt;Object.Instantiate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Destroy&lt;/code&gt; the instance you own&lt;/td&gt;
&lt;td&gt;Treating deactivation as destruction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Material, Mesh, Texture2D, or ScriptableObject you created&lt;/td&gt;
&lt;td&gt;The owner calls &lt;code&gt;Destroy&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Destroying a shared asset you only reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Addressables.LoadAssetAsync&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Addressables.Release&lt;/code&gt; the acquired handle&lt;/td&gt;
&lt;td&gt;Calling &lt;code&gt;Destroy&lt;/code&gt; on the asset instead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instance from &lt;code&gt;Addressables.InstantiateAsync&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;Addressables.ReleaseInstance&lt;/code&gt; for manual cleanup&lt;/td&gt;
&lt;td&gt;Confusing it with an ordinary &lt;code&gt;Instantiate&lt;/code&gt; clone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;NativeArray&lt;/code&gt; or similar allocation you own&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Dispose&lt;/code&gt;, respecting allocator lifetime and Job dependencies&lt;/td&gt;
&lt;td&gt;Disposing borrowed views or copies twice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Owned object from &lt;code&gt;new RenderTexture&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Stop using it; &lt;code&gt;Destroy&lt;/code&gt; for final cleanup, &lt;code&gt;Release&lt;/code&gt; for GPU resources only&lt;/td&gt;
&lt;td&gt;Assuming &lt;code&gt;Release&lt;/code&gt; destroys the object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RenderTexture.GetTemporary&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RenderTexture.ReleaseTemporary&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Destroying a borrowed resource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GraphicsBuffer&lt;/code&gt; or &lt;code&gt;ComputeBuffer&lt;/code&gt; you created&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Dispose&lt;/code&gt; or the corresponding &lt;code&gt;Release&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Only dropping the C# reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Owned file, network, or other &lt;code&gt;IDisposable&lt;/code&gt; resource&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;using&lt;/code&gt; or &lt;code&gt;Dispose&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Assuming the interface executes cleanup automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The following sections provide the API references. The rule is: &lt;strong&gt;pair acquisition with the matching cleanup, at the end of the agreed lifetime.&lt;/strong&gt; When passing a resource elsewhere, distinguish ownership transfer from borrowing.&lt;/p&gt;

&lt;p&gt;A generated &lt;code&gt;Texture2D&lt;/code&gt; and one loaded through Addressables have the same type but different cleanup requirements. Avoid a generic &lt;code&gt;ReleaseEverything(UnityEngine.Object obj)&lt;/code&gt; that decides solely from the type.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why memory remains even with garbage collection
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Assigning &lt;code&gt;null&lt;/code&gt; removes one reference
&lt;/h3&gt;

&lt;p&gt;An ordinary C# object becomes eligible for collection when it is no longer reachable. Setting one variable to &lt;code&gt;null&lt;/code&gt; does not help while another reference keeps it alive.&lt;sup id="fnref2"&gt;2&lt;/sup&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;data&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;byte&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="m"&gt;1024&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;anotherReference&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// anotherReference still refers to the same array.&lt;/span&gt;
&lt;span class="c1"&gt;// Setting data to null does not remove that other reference.&lt;/span&gt;
&lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anotherReference&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common retention paths include static collections and persistent services holding scene objects. Closing a screen does not remove references from those longer-lived owners, which can also retain the objects' associated data.&lt;/p&gt;

&lt;p&gt;Ask &lt;strong&gt;what still keeps the object reachable&lt;/strong&gt;, not whether you wrote &lt;code&gt;null&lt;/code&gt;. Memory Profiler identifies unintended references and missing cleanup as common causes of leaks.&lt;sup id="fnref3"&gt;3&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;List&amp;lt;T&amp;gt;.Clear()&lt;/code&gt; does not discard capacity
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Clear()&lt;/code&gt; sets the count to zero and removes references held by the elements, but retains &lt;code&gt;Capacity&lt;/code&gt;. Consider &lt;code&gt;TrimExcess()&lt;/code&gt; or changing &lt;code&gt;Capacity&lt;/code&gt; separately when reducing the backing storage is appropriate.&lt;sup id="fnref4"&gt;4&lt;/sup&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="c1"&gt;// Assume items is a List&amp;lt;ItemData&amp;gt; held by this class.&lt;/span&gt;
&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Consider this only at a boundary where large capacity is no longer needed.&lt;/span&gt;
&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrimExcess&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shrinking every frame only to grow again adds unnecessary work. Keep capacity for reusable working buffers; consider shrinking after unusually large temporary workloads at a screen or session boundary.&lt;/p&gt;

&lt;p&gt;Also, &lt;code&gt;List&amp;lt;GameObject&amp;gt;.Clear()&lt;/code&gt; does not destroy its GameObjects, and &lt;code&gt;List&amp;lt;IDisposable&amp;gt;.Clear()&lt;/code&gt; does not dispose its elements. &lt;strong&gt;Cleaning up each resource and removing collection references are separate steps.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Dispose&lt;/code&gt; is not another name for GC
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;IDisposable&lt;/code&gt; defines explicit resource cleanup. GC does not discover the interface and automatically call &lt;code&gt;Dispose()&lt;/code&gt;. The caller must arrange it through &lt;code&gt;using&lt;/code&gt; or &lt;code&gt;try/finally&lt;/code&gt;.&lt;sup id="fnref5"&gt;5&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Implementing &lt;code&gt;IDisposable&lt;/code&gt; on a &lt;code&gt;MonoBehaviour&lt;/code&gt; likewise does not make &lt;code&gt;Destroy&lt;/code&gt; call it. Connect disposal to your component's cleanup path explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. What &lt;code&gt;Destroy&lt;/code&gt; does—and does not—destroy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GameObject destruction is separate from asset cleanup
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Destroy(gameObject)&lt;/code&gt; destroys that GameObject, its components, and its Transform children. It does not recursively destroy every Material or Texture referenced by those components.&lt;sup id="fnref6"&gt;6&lt;/sup&gt;&lt;sup id="fnref7"&gt;7&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Ten character instances may share one Material and Texture. Destroying those assets when one character dies would break the other nine. Keep &lt;strong&gt;instance lifetime&lt;/strong&gt; separate from &lt;strong&gt;shared asset lifetime&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Actual destruction is deferred until after the current Update loop. Do not diagnose a leak from a measurement immediately after calling &lt;code&gt;Destroy&lt;/code&gt;, or replace it with &lt;code&gt;DestroyImmediate&lt;/code&gt; in runtime code. Account for deferred destruction in the lifecycle instead.&lt;sup id="fnref6"&gt;6&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Unity's &lt;code&gt;== null&lt;/code&gt; does not only mean “no C# reference”
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;UnityEngine.Object&lt;/code&gt; has a managed object and a corresponding native object. After native destruction, the managed object can remain: Unity's &lt;code&gt;== null&lt;/code&gt; may be &lt;code&gt;true&lt;/code&gt; while &lt;code&gt;ReferenceEquals(obj, null)&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;.&lt;sup id="fnref8"&gt;8&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;A destroyed component retained in a static field can still keep its large managed arrays alive. Memory Profiler exposes this kind of state as a &lt;code&gt;Leaked Managed Shell&lt;/code&gt;.&lt;sup id="fnref9"&gt;9&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Use Unity's operator when testing a Unity object's lifetime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;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="nf"&gt;DoSomething&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 &lt;code&gt;?.&lt;/code&gt; and &lt;code&gt;??&lt;/code&gt; operators do not perform Unity's destroyed-object check. &lt;code&gt;target?.DoSomething()&lt;/code&gt; is not a safe guard against a destroyed component. This does not prohibit those operators on ordinary C# objects.&lt;sup id="fnref8"&gt;8&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Putting everything in &lt;code&gt;OnDestroy&lt;/code&gt; is not enough
&lt;/h3&gt;

&lt;p&gt;Choose cleanup points according to the required lifetime.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Required lifetime&lt;/th&gt;
&lt;th&gt;Acquisition and cleanup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Only while enabled&lt;/td&gt;
&lt;td&gt;Start in &lt;code&gt;OnEnable&lt;/code&gt;; end in &lt;code&gt;OnDisable&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;While the component exists&lt;/td&gt;
&lt;td&gt;Acquire during initialization; use explicit cleanup / &lt;code&gt;OnDestroy&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One operation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;using&lt;/code&gt; or &lt;code&gt;try/finally&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Across multiple screens or scenes&lt;/td&gt;
&lt;td&gt;An explicit, longer-lived owner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;OnDestroy&lt;/code&gt; is only called for GameObjects that were previously active. If another system injects resources into an object that remains inactive, that system needs an explicit cleanup path rather than relying only on &lt;code&gt;OnDestroy&lt;/code&gt;.&lt;sup id="fnref10"&gt;10&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The examples below are ordinary runtime components. &lt;code&gt;ExecuteAlways&lt;/code&gt; and Editor extensions require cleanup paths beyond normal Play Mode lifecycles.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Track the Materials, Meshes, and Textures you create
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Some getters can create instances
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;renderer.material&lt;/code&gt; can instantiate a Material. The first access to &lt;code&gt;MeshFilter.mesh&lt;/code&gt; can duplicate an existing Mesh or create a new one. Cleanup of the generated objects is your responsibility.&lt;sup id="fnref7"&gt;7&lt;/sup&gt;&lt;sup id="fnref11"&gt;11&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;This does &lt;strong&gt;not&lt;/strong&gt; mean every read of the same Renderer's &lt;code&gt;material&lt;/code&gt; creates another Material. The risk is overlooking the initial instantiation or later replacement and recreation.&lt;/p&gt;

&lt;p&gt;For reference-only access, consider &lt;code&gt;sharedMaterial&lt;/code&gt; and &lt;code&gt;sharedMesh&lt;/code&gt;. Changes to shared objects affect other users; sharing is not permission to modify them indiscriminately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: destroy only the Material you explicitly cloned
&lt;/h3&gt;

&lt;p&gt;Save this as &lt;code&gt;RuntimeMaterialOwner.cs&lt;/code&gt; and attach it to a MeshRenderer with one Material. This component assumes exclusive control of the Renderer's first Material slot.&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;RequireComponent&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;MeshRenderer&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;RuntimeMaterialOwner&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;MeshRenderer&lt;/span&gt; &lt;span class="n"&gt;_renderer&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;Material&lt;/span&gt; &lt;span class="n"&gt;_originalMaterial&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;Material&lt;/span&gt; &lt;span class="n"&gt;_ownedMaterial&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;_renderer&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;MeshRenderer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;_originalMaterial&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sharedMaterial&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;_originalMaterial&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Assign a source Material to clone."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Create the owned Material explicitly instead of relying on an implicit clone.&lt;/span&gt;
        &lt;span class="n"&gt;_ownedMaterial&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;Material&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_originalMaterial&lt;/span&gt;&lt;span class="p"&gt;)&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;_originalMaterial&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;" (Owned Runtime)"&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="n"&gt;_renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sharedMaterial&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_ownedMaterial&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;ReleaseOwnedMaterial&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Material&lt;/span&gt; &lt;span class="n"&gt;owned&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_ownedMaterial&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_ownedMaterial&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_renderer&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;_renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sharedMaterial&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;owned&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;owned&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;_renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sharedMaterial&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_originalMaterial&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;_originalMaterial&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owned&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owned&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;OnDestroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;ReleaseOwnedMaterial&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;Cleanup destroys &lt;strong&gt;the Material recorded when it was created&lt;/strong&gt;, without accessing &lt;code&gt;renderer.material&lt;/code&gt; again. Calling &lt;code&gt;ReleaseOwnedMaterial()&lt;/code&gt; before &lt;code&gt;OnDestroy&lt;/code&gt; leaves the ownership field empty, so the later call does not destroy it again.&lt;/p&gt;

&lt;p&gt;Do not share this owned Material with other objects. To share it, move ownership to something that outlives every user. Cloning a Material does not give you ownership of its Textures. Retain any Addressables handles needed while the source assets are used.&lt;/p&gt;

&lt;p&gt;Apply the same pattern to &lt;code&gt;new Mesh&lt;/code&gt;, &lt;code&gt;new Texture2D&lt;/code&gt;, and &lt;code&gt;ScriptableObject.CreateInstance&lt;/code&gt;. Do not include Inspector-assigned shared assets in this cleanup indiscriminately.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Match Addressables acquisitions with releases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Release&lt;/code&gt; relinquishes an acquired reference
&lt;/h3&gt;

&lt;p&gt;Addressables tracks assets and AssetBundles with its own reference counts. Explicit loads need corresponding releases; assigning a C# variable to &lt;code&gt;null&lt;/code&gt; does not decrement those counts.&lt;sup id="fnref12"&gt;12&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Retain a handle while using its result. Copying the handle does not acquire an independent reference. An additional owner needs an explicit acquisition, such as a separate load or the documented &lt;code&gt;Acquire&lt;/code&gt; operation, and a matching release.&lt;sup id="fnref13"&gt;13&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IsValid()&lt;/code&gt; is &lt;strong&gt;not an ownership flag telling you whether your caller still owes a release&lt;/strong&gt;. Another user can retain the same underlying operation. Track your own ownership and released state separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: release a screen's acquisition even during loading
&lt;/h3&gt;

&lt;p&gt;Save this as &lt;code&gt;AddressableRawImage.cs&lt;/code&gt; and attach it to a UGUI RawImage. Install UGUI and Addressables, mark a Texture as Addressable, and set its address. This component alone writes the RawImage's &lt;code&gt;texture&lt;/code&gt;, retaining the asset only while the component is active and enabled.&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;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine.AddressableAssets&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.ResourceManagement.AsyncOperations&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.UI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;RequireComponent&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;RawImage&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;AddressableRawImage&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;string&lt;/span&gt; &lt;span class="n"&gt;_address&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;RawImage&lt;/span&gt; &lt;span class="n"&gt;_image&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;AsyncOperationHandle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Texture2D&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_handle&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;_ownsHandle&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;_image&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;RawImage&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;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnEnable&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_address&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Set the Texture address."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;_handle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Addressables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadAssetAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Texture2D&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_ownsHandle&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;_handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Completed&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;OnCompleted&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;OnCompleted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AsyncOperationHandle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Texture2D&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;handle&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;_ownsHandle&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;Equals&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="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="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;AsyncOperationStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Succeeded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;error&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;OperationException&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;
                &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"Unknown loading error"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nf"&gt;ReleaseLoad&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Failed to load Texture: &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;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_image&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;ReleaseLoad&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;_image&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;handle&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="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;ReleaseLoad&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;_ownsHandle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;var&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;_handle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_ownsHandle&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;_handle&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="c1"&gt;// Stop using the asset before releasing the handle.&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;_image&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;_image&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="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&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;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;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Completed&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;OnCompleted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;Addressables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Release&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="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;ReleaseLoad&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;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ReleaseLoad&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 example releases failed operations as well as successful ones: failed handles still have operation instance data to release.&lt;sup id="fnref13"&gt;13&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Releasing before completion does not immediately cancel the underlying load. Addressables allows a pending acquisition to be released, with cleanup occurring after completion. The example also removes the callback so it cannot update a closed screen.&lt;sup id="fnref14"&gt;14&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not confuse ordinary &lt;code&gt;Instantiate&lt;/code&gt; with &lt;code&gt;InstantiateAsync&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Cloning a Prefab loaded through &lt;code&gt;LoadAssetAsync&amp;lt;GameObject&amp;gt;&lt;/code&gt; with ordinary &lt;code&gt;Object.Instantiate&lt;/code&gt; &lt;strong&gt;does not automatically increment Addressables' reference count for that clone&lt;/strong&gt;. Releasing the load handle while clones remain can invalidate the lifetimes of their dependent Materials and Textures.&lt;sup id="fnref15"&gt;15&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Keep the Prefab load handle until all owned clones have finished being destroyed. Because &lt;code&gt;Destroy&lt;/code&gt; is deferred, do not release the final handle immediately after requesting destruction. Inactive instances waiting in a pool still count as users.&lt;/p&gt;

&lt;p&gt;For manual cleanup of &lt;code&gt;Addressables.InstantiateAsync&lt;/code&gt; instances, use &lt;code&gt;ReleaseInstance&lt;/code&gt;. With the default &lt;code&gt;trackHandle: true&lt;/code&gt;, you can pass the instance; with &lt;code&gt;false&lt;/code&gt;, retain the operation handle and pass it to &lt;code&gt;ReleaseInstance(handle)&lt;/code&gt; instead. Do not release twice through different cleanup paths.&lt;sup id="fnref16"&gt;16&lt;/sup&gt;&lt;sup id="fnref17"&gt;17&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Scene unloading can automatically release instances created with &lt;code&gt;trackHandle: true&lt;/code&gt;. That does not mean every asset separately acquired through &lt;code&gt;LoadAssetAsync&lt;/code&gt; is automatically released too.&lt;sup id="fnref18"&gt;18&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A zero reference count does not guarantee an immediate memory drop
&lt;/h3&gt;

&lt;p&gt;An asset whose reference count reaches zero may remain in memory while another asset in the same AssetBundle is still used. Inspect asset and Bundle dependencies, not only individual counts.&lt;sup id="fnref12"&gt;12&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Packing persistent assets and large screen-specific assets together makes their lifetimes harder to separate. Once acquisitions and releases balance, consider grouping Bundle contents by lifetime.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Unloading a scene is not the same as unloading unused assets
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;UnloadSceneAsync&lt;/code&gt; does not unload every asset
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;SceneManager.UnloadSceneAsync&lt;/code&gt; destroys the GameObjects belonging to the scene and removes it from SceneManager. The API explicitly states that assets are not unloaded by this operation alone.&lt;sup id="fnref19"&gt;19&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;For ordinary scene management, consider waiting for scene unloading, removing unnecessary references, then waiting for &lt;code&gt;Resources.UnloadUnusedAssets()&lt;/code&gt; at a suitable loading-screen 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="c1"&gt;// Excerpt from a coroutine.&lt;/span&gt;
&lt;span class="c1"&gt;// sceneToUnload is a loaded Scene managed by ordinary SceneManager APIs.&lt;/span&gt;
&lt;span class="c1"&gt;// Run this outside the scene being unloaded, with another scene kept loaded.&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;unload&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SceneManagement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SceneManager&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UnloadSceneAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sceneToUnload&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;unload&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;unload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// By this point, owners must have cleared unnecessary caches and references.&lt;/span&gt;
&lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UnloadUnusedAssets&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Addressables-loaded scenes, use Addressables' scene-unloading API and handle management instead. This snippet is not a replacement for that cleanup.&lt;sup id="fnref18"&gt;18&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;UnloadUnusedAssets&lt;/code&gt; is not a universal “free everything” button
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Resources.UnloadUnusedAssets&lt;/code&gt; examines references through the GameObject hierarchy, script components, and static variables to identify unused assets. A long-lived cache may keep an otherwise unnecessary asset referenced.&lt;sup id="fnref20"&gt;20&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The script execution stack is not examined.&lt;/strong&gt; Do not equate this with C# GC reachability and assume a local variable protects the asset. Keep required assets in references Unity tracks, such as fields on an appropriate owner; preserve the corresponding Addressables acquisition too.&lt;sup id="fnref20"&gt;20&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Do not make this a per-frame cleanup operation. Measure its necessity and cost at suitable transitions. Following it with &lt;code&gt;GC.Collect()&lt;/code&gt; still does not reclaim every category of memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Direct asset and AssetBundle unloading also requires ownership
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Resources.UnloadAsset&lt;/code&gt; targets assets stored on disk and invalidates the specified asset. Ensure no other user still needs it. It is not a general cleanup API for arbitrary runtime-created objects.&lt;sup id="fnref21"&gt;21&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;For manually managed AssetBundles, &lt;code&gt;Unload(false)&lt;/code&gt; leaves loaded assets alive; &lt;code&gt;Unload(true)&lt;/code&gt; destroys them and can break active users. Unloading with &lt;code&gt;false&lt;/code&gt; and reloading can also produce duplicate assets. Do not manually unload Bundles owned by Addressables.&lt;sup id="fnref22"&gt;22&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Manage &lt;code&gt;NativeArray&lt;/code&gt; ownership alongside its final Job dependency
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Respect the allocator's lifetime
&lt;/h3&gt;

&lt;p&gt;Using an unmanaged allocation from C# does not make GC responsible for it. Collections allocators have different lifetime requirements.&lt;sup id="fnref23"&gt;23&lt;/sup&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Allocator&lt;/th&gt;
&lt;th&gt;Lifetime and constraints&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Temp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Use within the allocating thread and scope. Main-thread allocations are reclaimed at the end of the frame; Job-local allocations at the end of that Job. Do not pass main-thread Temp allocations into Jobs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TempJob&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dispose within four frames of allocation. The deadline is not a promise of automatic, safe cleanup.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Persistent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retain as long as needed, then dispose explicitly.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Avoid keeping &lt;code&gt;Temp&lt;/code&gt; allocations across network waits or &lt;code&gt;await&lt;/code&gt;. A short-lived allocation must not depend on an unpredictable delay.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: dispose only after the Job completes
&lt;/h3&gt;

&lt;p&gt;Save this as &lt;code&gt;NativeJobOwner.cs&lt;/code&gt; and attach it to a GameObject. It owns an array while enabled and waits for the array's Job before disposing on disable.&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.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;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;NativeJobOwner&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;NativeArray&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;_values&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;_lastUse&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;struct&lt;/span&gt; &lt;span class="nc"&gt;FillJob&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IJobParallelFor&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="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;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;Values&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;index&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="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;OnEnable&lt;/span&gt;&lt;span class="p"&gt;()&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="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="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="m"&gt;256&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="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_lastUse&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;FillJob&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;_values&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Schedule&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;Length&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="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;ReleaseOwnedData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ReleaseOwnedData&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;_values&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="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="c1"&gt;// Wait for the last Job using the array before disposing it.&lt;/span&gt;
        &lt;span class="n"&gt;_lastUse&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;_values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;_values&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;_lastUse&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="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;ReleaseOwnedData&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;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ReleaseOwnedData&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;Complete()&lt;/code&gt; blocks the caller when necessary. This example prioritizes safe cleanup. When several Jobs use the allocation, combine their dependencies so &lt;code&gt;_lastUse&lt;/code&gt; really represents every outstanding user.&lt;sup id="fnref24"&gt;24&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;To defer waiting, &lt;code&gt;Dispose(JobHandle)&lt;/code&gt; can schedule a disposal Job. After scheduling disposal, do not pass the array to new work, and track the returned handle. Complete the disposal Job at the appropriate shutdown or synchronization boundary.&lt;sup id="fnref25"&gt;25&lt;/sup&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="c1"&gt;// values is an owned NativeArray; lastUse includes all Jobs that use it.&lt;/span&gt;
&lt;span class="n"&gt;JobHandle&lt;/span&gt; &lt;span class="n"&gt;disposal&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="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastUse&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="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Do not use values or any aliases after scheduling disposal.&lt;/span&gt;
&lt;span class="c1"&gt;// To avoid waiting here, retain disposal in the owner and Complete it later.&lt;/span&gt;
&lt;span class="n"&gt;disposal&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Copying the struct does not duplicate the allocation
&lt;/h3&gt;

&lt;p&gt;Here, &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; do not own separate array storage:&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;a&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="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="m"&gt;16&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;a&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="c1"&gt;// Invalid: b.Dispose();  This tries to dispose the same allocation through a copy.&lt;/span&gt;
&lt;span class="c1"&gt;// Invalid: Debug.Log(b[0]);  This tries to access memory already disposed.&lt;/span&gt;
&lt;span class="n"&gt;b&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;Disposal updates &lt;code&gt;IsCreated&lt;/code&gt; on the struct being disposed, not every copy. Another copy may still report &lt;code&gt;true&lt;/code&gt;. &lt;strong&gt;An &lt;code&gt;IsCreated&lt;/code&gt; check does not make disposal through arbitrary copies safe.&lt;/strong&gt;&lt;sup id="fnref23"&gt;23&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Similarly, &lt;code&gt;Texture2D.GetRawTextureData&amp;lt;T&amp;gt;()&lt;/code&gt; returns a direct &lt;code&gt;NativeArray&lt;/code&gt; view of the Texture's data. Do not dispose it as an owned allocation, and respect invalidation caused by modifying or destroying the Texture. The non-generic &lt;code&gt;GetRawTextureData()&lt;/code&gt; overload instead returns a &lt;code&gt;byte[]&lt;/code&gt; copy; the shared name does not imply shared ownership rules.&lt;sup id="fnref26"&gt;26&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. GPU resources need more than dropping C# references
&lt;/h2&gt;

&lt;h3&gt;
  
  
  RenderTexture &lt;code&gt;Release&lt;/code&gt; and &lt;code&gt;Destroy&lt;/code&gt; have different roles
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;RenderTexture.Release()&lt;/code&gt; frees hardware resources but does not destroy the RenderTexture object. Using it again can recreate those GPU resources.&lt;sup id="fnref27"&gt;27&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;For a RenderTexture you created with &lt;code&gt;new RenderTexture&lt;/code&gt;, final cleanup might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cleanup excerpt. This owner created _ownedRt with new RenderTexture.&lt;/span&gt;
&lt;span class="c1"&gt;// This example assumes the Camera is the only consumer.&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;_ownedRt&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_camera&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;_camera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;targetTexture&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;_ownedRt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_camera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;targetTexture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RenderTexture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;_ownedRt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;RenderTexture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&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;_ownedRt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Release&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;_ownedRt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_ownedRt&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This explicitly releases GPU storage before destroying the object. It does not mean every final &lt;code&gt;Destroy&lt;/code&gt; requires a preceding &lt;code&gt;Release&lt;/code&gt;. Distinguish &lt;strong&gt;releasing GPU storage for later reuse&lt;/strong&gt; from &lt;strong&gt;ending the object's lifetime&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Also stop use through RawImage, Materials, global Shader properties, or other consumers. Do not apply this cleanup to a RenderTexture owned by someone else.&lt;/p&gt;

&lt;h3&gt;
  
  
  Return &lt;code&gt;GetTemporary&lt;/code&gt; resources with &lt;code&gt;ReleaseTemporary&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;RenderTexture.GetTemporary&lt;/code&gt; borrows from Unity's internal pool. Return it with &lt;code&gt;ReleaseTemporary&lt;/code&gt;, including on exceptions or early returns.&lt;sup id="fnref28"&gt;28&lt;/sup&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="c1"&gt;// Synchronous temporary-work excerpt, not a rendering pipeline RenderPass.&lt;/span&gt;
&lt;span class="n"&gt;RenderTexture&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;RenderTexture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;RenderTexture&lt;/span&gt; &lt;span class="n"&gt;temporary&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RenderTexture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTemporary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;256&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="n"&gt;RenderTexture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;temporary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;GL&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;true&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;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Perform temporary work that finishes within this scope.&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;RenderTexture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&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;RenderTexture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReleaseTemporary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temporary&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 keep using a cached reference after returning it, or destroy the borrowed RenderTexture yourself. Another operation may reuse it after return.&lt;/p&gt;

&lt;h3&gt;
  
  
  Explicitly clean up GraphicsBuffer and ComputeBuffer
&lt;/h3&gt;

&lt;p&gt;Call &lt;code&gt;Dispose&lt;/code&gt; or the corresponding &lt;code&gt;Release&lt;/code&gt; on buffers you created when they are no longer used. Dropping C# references is not sufficient GPU resource management.&lt;sup id="fnref29"&gt;29&lt;/sup&gt;&lt;sup id="fnref30"&gt;30&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;First stop scheduling rendering or compute work that uses the buffer. Async GPU readback and other outstanding operations can extend its required lifetime beyond the screen that requested it.&lt;/p&gt;

&lt;p&gt;URP Render Graph also distinguishes resources created inside the graph from imported external resources. Do not independently destroy graph-managed resources, or assume importing a resource transfers ownership of a custom RTHandle to the graph.&lt;sup id="fnref31"&gt;31&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Discard CPU copies only when you no longer need CPU access
&lt;/h3&gt;

&lt;p&gt;Runtime-generated Textures and Meshes may retain CPU copies alongside GPU data. Once CPU reads and edits are finished, these APIs can discard that storage:&lt;sup id="fnref32"&gt;32&lt;/sup&gt;&lt;sup id="fnref33"&gt;33&lt;/sup&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="c1"&gt;// Only after creation and editing are finished, with no further CPU reads or writes.&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;true&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;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;mesh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UploadMeshData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;markNoLongerReadable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not end the Texture or Mesh's lifetime. GPU data remains available for rendering, and final object cleanup is still separate. Do not apply this when later code needs to read pixels or edit vertices.&lt;/p&gt;

&lt;p&gt;For Textures, discarding the CPU copy also fixes the uploaded resolution: subsequent mipmap-limit changes no longer affect it. Do not apply the option indiscriminately just to reduce memory.&lt;sup id="fnref32"&gt;32&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Events, async operations, and pools can retain resources
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A long-lived event publisher can retain its subscribers
&lt;/h3&gt;

&lt;p&gt;An event publisher retains subscribers through registered delegates. If the publisher survives, a forgotten subscription can keep an unnecessary subscriber reachable.&lt;sup id="fnref34"&gt;34&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The problem is not “events always leak,” but &lt;strong&gt;a long-lived publisher retaining a shorter-lived subscriber&lt;/strong&gt;. For notifications needed only while a screen is enabled, pair subscription and unsubscription as follows.&lt;/p&gt;

&lt;p&gt;Save this as &lt;code&gt;ScoreObserver.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;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;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;ScoreEvents&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;event&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Changed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Publish&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;score&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;Changed&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&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="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;SubsystemRegistration&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;ResetForPlayMode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Changed&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="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;ScoreObserver&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnEnable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ScoreEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Changed&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;OnScoreChanged&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;{&lt;/span&gt;
        &lt;span class="n"&gt;ScoreEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Changed&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;OnScoreChanged&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;OnScoreChanged&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;score&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;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Score: &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="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If notifications are needed while disabled, this lifetime is wrong: use the component's full lifetime or an explicit session instead.&lt;/p&gt;

&lt;p&gt;Do not assume that writing a similar anonymous lambda during unsubscription removes the original registration. Use a named method or retain the registered delegate when unsubscription is required.&lt;sup id="fnref34"&gt;34&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;With Domain Reload disabled, static variables and events survive between Play Mode sessions. The reset above clears the event between sessions, but &lt;strong&gt;resetting a static field does not release native resources&lt;/strong&gt;. Clean up owned buffers or load handles before discarding their references.&lt;sup id="fnref35"&gt;35&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cancellation and resource cleanup are separate operations
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;MonoBehaviour.destroyCancellationToken&lt;/code&gt; corresponds to destruction of that component. Retrieve and retain it before destruction, then pass it to async APIs that accept cancellation. Disabling is not destruction: a UI that merely closes also needs a screen-session lifetime.&lt;sup id="fnref36"&gt;36&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;For an owned &lt;code&gt;CancellationTokenSource&lt;/code&gt;, &lt;code&gt;Cancel()&lt;/code&gt; and &lt;code&gt;Dispose()&lt;/code&gt; are separate. &lt;code&gt;Dispose()&lt;/code&gt; does not request cancellation. Request cancellation, confirm users have finished, and dispose when it no longer races with other operations.&lt;sup id="fnref37"&gt;37&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Work that cannot be canceled, or that completes late, still needs cleanup for unused results and handles. The Addressables example separates “do not update the screen” from “release the acquisition.”&lt;/p&gt;

&lt;p&gt;Coroutines stop when their GameObject becomes inactive or their MonoBehaviour is destroyed, but not merely because &lt;code&gt;MonoBehaviour.enabled = false&lt;/code&gt;. Cleanup placed after a &lt;code&gt;yield&lt;/code&gt; is not guaranteed to run on every exit path.&lt;sup id="fnref38"&gt;38&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Dispose owned &lt;code&gt;UnityWebRequest&lt;/code&gt; instances on success, failure, interruption, and screen destruction. The request and resources obtained from its result, such as a Texture, can have separate lifetimes.&lt;sup id="fnref39"&gt;39&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pools retain objects for reuse; returning is not final cleanup
&lt;/h3&gt;

&lt;p&gt;Object pools reduce repeated creation and destruction by keeping objects available. Returning an object is therefore different from finally releasing its resources.&lt;sup id="fnref40"&gt;40&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;On return, reset references to previous users, event subscriptions, and running operations. Whether large buffers should remain allocated depends on reuse frequency and the memory budget.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ObjectPool&amp;lt;T&amp;gt;.maxSize&lt;/code&gt; limits how many returned objects the pool retains, not the total number that can be checked out simultaneously. Enforce a separate checkout limit when you need a total memory cap.&lt;sup id="fnref41"&gt;41&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Clear()&lt;/code&gt; acts on pooled objects and invokes a configured &lt;code&gt;actionOnDestroy&lt;/code&gt;. Storing GameObjects does not automatically make the pool destroy them without that callback. Checked-out objects need separate tracking and cleanup.&lt;sup id="fnref40"&gt;40&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Verify cleanup by returning to the same state
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GC Alloc and leaks measure different problems
&lt;/h3&gt;

&lt;p&gt;High per-frame &lt;code&gt;GC Alloc&lt;/code&gt; indicates allocation churn and potential GC work. Near-zero &lt;code&gt;GC Alloc&lt;/code&gt; does not prove that previously allocated Textures, native arrays, or caches have been released.&lt;/p&gt;

&lt;p&gt;Unity 6.3's incremental GC spreads collection work across frames to reduce long pauses. It does not collect reachable objects or fix missing Addressables releases. The Web platform does not support incremental GC.&lt;sup id="fnref42"&gt;42&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Before adding periodic &lt;code&gt;GC.Collect()&lt;/code&gt;, investigate retained references, unreleased handles, and native resources that lack cleanup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Distinguish used, reserved, and OS-reported memory
&lt;/h3&gt;

&lt;p&gt;Unity reserves memory for reuse. Used and reserved memory differ, and managed heap space is not guaranteed to return to the OS at a particular time.&lt;sup id="fnref43"&gt;43&lt;/sup&gt;&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;A process memory number that does not immediately fall is not proof of a leak. Conversely, spare capacity in reserved memory can temporarily hide a growing leak from that same metric.&lt;/p&gt;

&lt;p&gt;Inspect unwanted object counts, sizes, reference paths, and loaded assets and handles first. Unity cannot fully track all driver and platform allocations, so combine these observations with target-platform measurements.&lt;sup id="fnref43"&gt;43&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A repeatable measurement procedure
&lt;/h3&gt;

&lt;p&gt;Use Memory Profiler snapshot comparisons taken &lt;strong&gt;after returning to the same screen and state&lt;/strong&gt;.&lt;sup id="fnref3"&gt;3&lt;/sup&gt;&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;Procedure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Warm-up&lt;/td&gt;
&lt;td&gt;Open and close the target screen or scene once to exercise initial loads and caches.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot A&lt;/td&gt;
&lt;td&gt;Return to the baseline screen and wait for normal cleanup to finish.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeat&lt;/td&gt;
&lt;td&gt;Perform the same open/close or scene-transition sequence, for example ten times.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot B&lt;/td&gt;
&lt;td&gt;Return to A's state and wait for the same completion conditions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;More repetitions and C&lt;/td&gt;
&lt;td&gt;Repeat again to distinguish a plateau from continued accumulation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Investigate&lt;/td&gt;
&lt;td&gt;Trace growing types and instances to owners, references, cleanup paths, and Addressables dependencies.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ten repetitions are an example, not a universal pass threshold. Choose counts and tolerances based on usage, device budgets, and intended caching.&lt;/p&gt;

&lt;p&gt;Do not compare snapshots with pending destruction, unloading, async loading, or disposal Jobs against snapshots where all cleanup has completed. If normal cleanup includes &lt;code&gt;UnloadUnusedAssets&lt;/code&gt;, wait for it every time.&lt;/p&gt;

&lt;p&gt;Keep ordinary behavior tests separate from diagnostic tests that force GC or additional unloading. Extra cleanup used only in a test does not prove the normal lifecycle releases resources correctly.&lt;/p&gt;

&lt;p&gt;Editor snapshots include the Editor's own memory. Use them for quick investigation, but perform final checks against a Player, such as a Development Build, on the target device and scripting backend.&lt;sup id="fnref44"&gt;44&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Narrow the investigation by symptom
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;First places to investigate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Material/Mesh counts grow after every screen closure&lt;/td&gt;
&lt;td&gt;Implicit &lt;code&gt;material&lt;/code&gt;/&lt;code&gt;mesh&lt;/code&gt; clones, ownership fields, missing destruction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GameObjects disappear but large managed arrays remain&lt;/td&gt;
&lt;td&gt;Statics, events, persistent services, &lt;code&gt;Leaked Managed Shell&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Textures remain after Addressables releases&lt;/td&gt;
&lt;td&gt;Unreleased handles, other assets in the same Bundle, dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native memory keeps growing&lt;/td&gt;
&lt;td&gt;Owned native containers, disposal Job dependencies, buffer cleanup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rendering breaks after adding cleanup&lt;/td&gt;
&lt;td&gt;Destroyed shared assets, early final handle release, cleanup during borrowing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory rises and then plateaus&lt;/td&gt;
&lt;td&gt;Intended pools, caches, or reservation—or unnecessary retention&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are starting points, not diagnoses. Identify who owns the remaining resource and why it is still retained.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: decide ownership before choosing a cleanup API
&lt;/h2&gt;

&lt;p&gt;Unity memory management cannot be reduced to “set everything to &lt;code&gt;null&lt;/code&gt;,” “destroy everything,” or “run GC at the end.” Ask three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Who acquired it, and how?&lt;/strong&gt; Distinguish creation, loading, and borrowing; match the cleanup method.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who still needs it?&lt;/strong&gt; Include shared users, pooled instances, Jobs, and async work—not just the screen that created it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does every exit path release exactly what it owns?&lt;/strong&gt; Cover success, failure, cancellation, disabling, and destruction.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then verify repeated transitions back to the same state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preventing leaks and preventing premature cleanup both begin with explicit ownership and lifetimes.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;These are the primary sources cited by the Japanese edition. Unity URLs generally target 6000.3; package URLs retain their documented version series. They are preserved here rather than silently updated to newer releases.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Unity 6.3 Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/performance-memory-overview.html" rel="noopener noreferrer"&gt;Memory in Unity introduction&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Unity 6.3 Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/performance-managed-memory-introduction.html" rel="noopener noreferrer"&gt;Managed memory introduction&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;Memory Profiler 1.1, &lt;a href="https://docs.unity3d.com/Packages/com.unity.memoryprofiler@1.1/manual/find-memory-leaks.html" rel="noopener noreferrer"&gt;Find memory leaks&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;Microsoft Learn, &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.clear?view=net-10.0" rel="noopener noreferrer"&gt;&lt;code&gt;List&amp;lt;T&amp;gt;.Clear&lt;/code&gt;&lt;/a&gt;. The cited contract concerns element references, Count, and Capacity; this does not equate Unity's runtime with .NET 10.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;Microsoft Learn, &lt;a href="https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/using-objects" rel="noopener noreferrer"&gt;Using objects that implement IDisposable&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Object.Destroy.html" rel="noopener noreferrer"&gt;Object.Destroy&lt;/a&gt;, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Object.DestroyImmediate.html" rel="noopener noreferrer"&gt;Object.DestroyImmediate&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Renderer-material.html" rel="noopener noreferrer"&gt;Renderer.material&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Object.html" rel="noopener noreferrer"&gt;Object&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn9"&gt;
&lt;p&gt;Memory Profiler 1.1, &lt;a href="https://docs.unity3d.com/Packages/com.unity.memoryprofiler@1.1/manual/managed-shell-objects.html" rel="noopener noreferrer"&gt;Analyze managed shell objects&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn10"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/MonoBehaviour.OnDestroy.html" rel="noopener noreferrer"&gt;MonoBehaviour.OnDestroy&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn11"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/MeshFilter-mesh.html" rel="noopener noreferrer"&gt;MeshFilter.mesh&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn12"&gt;
&lt;p&gt;Addressables 2.10, &lt;a href="https://docs.unity3d.com/Packages/com.unity.addressables@2.10/manual/memory-assets.html" rel="noopener noreferrer"&gt;Managing Addressable asset memory&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn13"&gt;
&lt;p&gt;Addressables 2.10, &lt;a href="https://docs.unity3d.com/Packages/com.unity.addressables@2.10/manual/AddressableAssetsAsyncOperationHandle.html" rel="noopener noreferrer"&gt;Wait for asynchronous loads to complete&lt;/a&gt;, &lt;a href="https://docs.unity3d.com/Packages/com.unity.addressables@2.10/api/UnityEngine.ResourceManagement.ResourceManager.html#UnityEngine_ResourceManagement_ResourceManager_Acquire_UnityEngine_ResourceManagement_AsyncOperations_AsyncOperationHandle_" rel="noopener noreferrer"&gt;ResourceManager.Acquire&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn14"&gt;
&lt;p&gt;Addressables 2.10, &lt;a href="https://docs.unity3d.com/Packages/com.unity.addressables@2.10/manual/load-wait-asynchronous-coroutines.html" rel="noopener noreferrer"&gt;Wait for asynchronous loads with coroutines&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn15"&gt;
&lt;p&gt;Addressables 2.10, &lt;a href="https://docs.unity3d.com/Packages/com.unity.addressables@2.10/manual/load-assets.html" rel="noopener noreferrer"&gt;Load assets&lt;/a&gt;. For an explicit explanation of reference counting, also see the official Addressables 1.18 section &lt;a href="https://docs.unity3d.com/Packages/com.unity.addressables@1.18/manual/LoadingAddressableAssets.html#instantiating-objects-from-addressables" rel="noopener noreferrer"&gt;Instantiating objects from Addressables&lt;/a&gt;. Its older sample code is not reproduced as the implementation for this article.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn16"&gt;
&lt;p&gt;Addressables 2.10 API, &lt;a href="https://docs.unity3d.com/Packages/com.unity.addressables@2.10/api/UnityEngine.AddressableAssets.Addressables.InstantiateAsync.html" rel="noopener noreferrer"&gt;Addressables.InstantiateAsync&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn17"&gt;
&lt;p&gt;Addressables 2.10 API, &lt;a href="https://docs.unity3d.com/Packages/com.unity.addressables@2.10/api/UnityEngine.AddressableAssets.Addressables.ReleaseInstance.html" rel="noopener noreferrer"&gt;Addressables.ReleaseInstance&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn18"&gt;
&lt;p&gt;Addressables 2.10, &lt;a href="https://docs.unity3d.com/Packages/com.unity.addressables@2.10/manual/UnloadingAddressableAssets.html" rel="noopener noreferrer"&gt;Unload Addressable assets&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn19"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/SceneManagement.SceneManager.UnloadSceneAsync.html" rel="noopener noreferrer"&gt;SceneManager.UnloadSceneAsync&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn20"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Resources.UnloadUnusedAssets.html" rel="noopener noreferrer"&gt;Resources.UnloadUnusedAssets&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn21"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Resources.UnloadAsset.html" rel="noopener noreferrer"&gt;Resources.UnloadAsset&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn22"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/AssetBundle.Unload.html" rel="noopener noreferrer"&gt;AssetBundle.Unload&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn23"&gt;
&lt;p&gt;Collections 2.6, &lt;a href="https://docs.unity3d.com/Packages/com.unity.collections@2.6/manual/allocator-overview.html" rel="noopener noreferrer"&gt;Allocator overview&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn24"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Unity.Jobs.JobHandle.Complete.html" rel="noopener noreferrer"&gt;JobHandle.Complete&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn25"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Unity.Collections.NativeArray_1.Dispose.html" rel="noopener noreferrer"&gt;&lt;code&gt;NativeArray&amp;lt;T&amp;gt;.Dispose&lt;/code&gt;&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn26"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Texture2D.GetRawTextureData.html" rel="noopener noreferrer"&gt;Texture2D.GetRawTextureData&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn27"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/RenderTexture.Release.html" rel="noopener noreferrer"&gt;RenderTexture.Release&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn28"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/RenderTexture.GetTemporary.html" rel="noopener noreferrer"&gt;RenderTexture.GetTemporary&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn29"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/GraphicsBuffer.html" rel="noopener noreferrer"&gt;GraphicsBuffer&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn30"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/ComputeBuffer.Release.html" rel="noopener noreferrer"&gt;ComputeBuffer.Release&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn31"&gt;
&lt;p&gt;Unity 6.3 URP Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/render-graph-create-a-texture.html" rel="noopener noreferrer"&gt;Create a texture in the Render Graph system&lt;/a&gt;, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/render-graph-import-a-texture.html" rel="noopener noreferrer"&gt;Import a texture into the Render Graph system&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn32"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Texture2D.Apply.html" rel="noopener noreferrer"&gt;Texture2D.Apply&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn33"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Mesh.UploadMeshData.html" rel="noopener noreferrer"&gt;Mesh.UploadMeshData&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn34"&gt;
&lt;p&gt;Microsoft Learn, &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/events/how-to-subscribe-to-and-unsubscribe-from-events" rel="noopener noreferrer"&gt;How to subscribe to and unsubscribe from events&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn35"&gt;
&lt;p&gt;Unity 6.3 Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/domain-reloading.html" rel="noopener noreferrer"&gt;Domain reloading&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn36"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/MonoBehaviour-destroyCancellationToken.html" rel="noopener noreferrer"&gt;MonoBehaviour.destroyCancellationToken&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn37"&gt;
&lt;p&gt;Microsoft Learn, &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtokensource.dispose?view=net-10.0" rel="noopener noreferrer"&gt;CancellationTokenSource.Dispose&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn38"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/MonoBehaviour.StopCoroutine.html" rel="noopener noreferrer"&gt;MonoBehaviour.StopCoroutine&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn39"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Networking.UnityWebRequest.Dispose.html" rel="noopener noreferrer"&gt;UnityWebRequest.Dispose&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn40"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Pool.ObjectPool_1.html" rel="noopener noreferrer"&gt;&lt;code&gt;ObjectPool&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn41"&gt;
&lt;p&gt;Unity 6.3 Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Pool.ObjectPool_1-ctor.html" rel="noopener noreferrer"&gt;&lt;code&gt;ObjectPool&amp;lt;T&amp;gt;&lt;/code&gt; constructor&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn42"&gt;
&lt;p&gt;Unity 6.3 Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/performance-incremental-garbage-collection.html" rel="noopener noreferrer"&gt;Garbage collection modes&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn43"&gt;
&lt;p&gt;Unity 6.3 Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/ProfilerMemory.html" rel="noopener noreferrer"&gt;Memory Profiler module reference&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn44"&gt;
&lt;p&gt;Memory Profiler 1.1, &lt;a href="https://docs.unity3d.com/Packages/com.unity.memoryprofiler@1.1/manual/snapshot-capture.html" rel="noopener noreferrer"&gt;Capture and import snapshots&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>csharp</category>
      <category>performance</category>
    </item>
    <item>
      <title>UE5 Shader Optimization: Materials, Custom HLSL, and Global Shaders with RDG</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Wed, 30 Sep 2026 06:24:27 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/ue5-shader-optimization-materials-custom-hlsl-and-global-shaders-with-rdg-28b0</link>
      <guid>https://dev.to/gamedevtoollab/ue5-shader-optimization-materials-custom-hlsl-and-global-shaders-with-rdg-28b0</guid>
      <description>&lt;p&gt;When a material graph grows in Unreal Engine 5, it is tempting to ask: would writing HLSL directly make it faster? Would implementing the shader in C++ make optimization easier?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Moving a shader into code is not, by itself, an optimization.&lt;/strong&gt; Material Editor nodes already become HLSL. Epic also warns that a Custom expression can prevent Unreal's material-level constant folding and produce more instructions than an equivalent graph built with standard nodes. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/essential-unreal-engine-material-concepts" rel="noopener noreferrer"&gt;Material concepts&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/custom-material-expressions-in-unreal-engine" rel="noopener noreferrer"&gt;Custom expressions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This guide separates five questions that are often mixed together: GPU execution time, shader permutation costs, first-use pipeline hitches, choosing an authoring layer, and adding a compute shader with C++ and HLSL.&lt;/p&gt;

&lt;p&gt;The engine baseline is &lt;strong&gt;Unreal Engine 5.8.2&lt;/strong&gt;, as in the Japanese edition dated August 31, 2026. Version-sensitive references were rechecked for this English edition on September 30, 2026. This is a documentation baseline, &lt;strong&gt;not a compilation or execution result for UE 5.8.2&lt;/strong&gt;. Epic's 5.8.2 hotfix announcement is dated August 25, 2026. &lt;a href="https://forums.unrealengine.com/t/5-8-2-hotfix-released/2746335" rel="noopener noreferrer"&gt;Release announcement&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The examples primarily target Windows, DirectX 12, and Shader Model 6. Mobile, consoles, and VR have different compiler, precision, bandwidth, and GPU constraints. Make the final decision on the actual target hardware.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Substrate:&lt;/strong&gt; New projects use Substrate by default from UE 5.7 onward, but Epic's UE 5.8 documentation still labels it Beta. Existing projects upgraded to UE 5.7 or later retain the non-Substrate path unless they explicitly opt in. The principles below apply to both paths; the code does not depend on a particular Substrate Slab. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/substrate-materials-in-unreal-engine" rel="noopener noreferrer"&gt;Substrate status&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/overview-of-substrate-materials-in-unreal-engine" rel="noopener noreferrer"&gt;Substrate overview&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Start with the simplest layer that meets your needs
&lt;/h2&gt;

&lt;p&gt;Do not begin by moving everything to HLSL or C++. Choose the layer that solves the actual problem.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;th&gt;Start with&lt;/th&gt;
&lt;th&gt;When to move lower&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ordinary surface shading&lt;/td&gt;
&lt;td&gt;Material nodes and Material Functions&lt;/td&gt;
&lt;td&gt;An operation is unavailable, or repeated logic is becoming difficult to maintain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Many parameter variations&lt;/td&gt;
&lt;td&gt;Material Instances&lt;/td&gt;
&lt;td&gt;Split the parent when static-switch combinations become unmanageable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A small custom formula&lt;/td&gt;
&lt;td&gt;Custom Material Expression&lt;/td&gt;
&lt;td&gt;Built-in nodes are insufficient, and generated HLSL plus measurements justify it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-screen color processing&lt;/td&gt;
&lt;td&gt;Post Process Material&lt;/td&gt;
&lt;td&gt;You need arbitrary UAVs or buffers, custom pass ordering, or compute dispatch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A compute shader&lt;/td&gt;
&lt;td&gt;Global Shader and RDG&lt;/td&gt;
&lt;td&gt;You need reads, writes, or dispatch control outside the material model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drawing meshes in a custom pass&lt;/td&gt;
&lt;td&gt;Mesh Material Shader or a custom mesh pass&lt;/td&gt;
&lt;td&gt;Renderer integration is necessary and its maintenance cost is acceptable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Something is simply “slow”&lt;/td&gt;
&lt;td&gt;Profiling&lt;/td&gt;
&lt;td&gt;Change the implementation after identifying the bottleneck and affected pass&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Material nodes are not a simplified substitute for “real” shaders. Expressions visually represent shader operations, and the graph is translated into HLSL. In the Material Editor, &lt;strong&gt;Window &amp;gt; Shader Code &amp;gt; HLSL Code&lt;/strong&gt; opens a read-only view of the generated source. It is not the final GPU-native instruction stream. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/essential-unreal-engine-material-concepts" rel="noopener noreferrer"&gt;Material concepts&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-material-editor-ui" rel="noopener noreferrer"&gt;Material Editor UI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The useful comparison is therefore not &lt;em&gt;nodes versus code&lt;/em&gt;. It is &lt;strong&gt;the shader compiled for the target platform, multiplied by the pixels, layers, and passes that execute it&lt;/strong&gt;. Use generated HLSL to investigate the implementation, then use target-platform statistics and GPU timings to evaluate performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate three kinds of shader cost
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Runtime GPU work
&lt;/h3&gt;

&lt;p&gt;Instruction count is only one part of GPU cost. Screen coverage, overdraw, texture access and cache behavior, arithmetic, special functions, loops, branch coherence, render-target traffic, and vertex work all matter. A material can also participate in several passes: depth, shadows, base pass, velocity, and representations used by Lumen. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/guidelines-for-optimizing-rendering-for-real-time-in-unreal-engine" rel="noopener noreferrer"&gt;Rendering optimization&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/shader-development-in-unreal-engine" rel="noopener noreferrer"&gt;Shader development&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A hypothetical 100-instruction shader applied once to a small prop is not equivalent to the same shader running across several full-screen translucent layers. The arithmetic is similar; the amount of work is not.&lt;/p&gt;

&lt;p&gt;Before changing a formula, ask where it runs, how often it runs, and what it reads and writes.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Compilation and shader permutations
&lt;/h3&gt;

&lt;p&gt;An Unreal material does not correspond to one shader binary. Rendering passes, vertex factories, feature levels, material quality, static parameters, and target platforms produce different variants. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/shader-development-in-unreal-engine" rel="noopener noreferrer"&gt;Shader development&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A Static Switch can remove its unused branch at compile time, which is useful for runtime performance. The trade-off is a separate compiled variation for each required static-parameter combination. Ten independent Boolean switches allow up to &lt;code&gt;2^10 = 1,024&lt;/code&gt; combinations before other permutation dimensions are considered. This is a theoretical combination count, not a claim that every project compiles all 1,024. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/material-parameter-expressions-in-unreal-engine" rel="noopener noreferrer"&gt;Material parameters&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A parent material that includes every possible feature can increase cooking time, Derived Data Cache (DDC) work, continuous-integration costs, shader storage, and loading overhead. Audit the combinations actually used rather than counting only parent assets. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-material-analyzer-tool" rel="noopener noreferrer"&gt;Material Analyzer&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. First-use PSO hitches
&lt;/h3&gt;

&lt;p&gt;Having shader binaries available does not mean every Pipeline State Object (PSO) is ready. Creating a required pipeline on first use can still cause driver-side work and a visible hitch. Removing a few pixel-shader instructions does not directly solve that problem. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/pso-precaching-for-unreal-engine" rel="noopener noreferrer"&gt;PSO precaching&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;UE's PSO Precaching system collects candidate pipelines and compiles them asynchronously. Epic's UE 5.8 documentation includes eligible Global Compute Shader permutations in precaching. You still need to verify platform support, wait for important outstanding requests during loading, and watch for missed or late PSOs during gameplay. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/pso-precaching-for-unreal-engine" rel="noopener noreferrer"&gt;PSO precaching&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are different optimization objectives. Decide whether the goal is &lt;strong&gt;less GPU time per frame&lt;/strong&gt;, &lt;strong&gt;less compilation and storage&lt;/strong&gt;, or &lt;strong&gt;fewer first-use hitches&lt;/strong&gt; before choosing a technique.&lt;/p&gt;

&lt;h2&gt;
  
  
  Profile outside the Material Editor
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Establish whether the GPU is the bottleneck
&lt;/h3&gt;

&lt;p&gt;Start with &lt;code&gt;stat unit&lt;/code&gt; and compare Game, Draw, GPU, and Frame times. When another stage dominates, reducing material instructions may not improve the overall frame time. The largest reported GPU value is a clue rather than a complete diagnosis: timing can include waits. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/guidelines-for-optimizing-rendering-for-real-time-in-unreal-engine" rel="noopener noreferrer"&gt;Rendering optimization&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;stat gpu&lt;/code&gt; and &lt;code&gt;ProfileGPU&lt;/code&gt; to identify the expensive passes, such as Base Pass, Translucency, Post Processing, Lumen, or shadows. Timing Insights in Unreal Insights can show CPU and GPU work on a shared timeline. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/guidelines-for-optimizing-rendering-for-real-time-in-unreal-engine" rel="noopener noreferrer"&gt;Rendering optimization&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/timing-insights-in-unreal-engine" rel="noopener noreferrer"&gt;Timing Insights&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep the comparison conditions fixed: resolution and screen percentage; dynamic resolution; VSync and frame limits; camera, visible objects, lights, and effects; scalability and material quality; RHI and shader model; and shader/PSO warm-up state.&lt;/p&gt;

&lt;p&gt;Save a reproducible scene and camera. Editor-only work and additional viewports can distort a comparison, so repeat the final test in an appropriate Development or Test build on the target platform, with the required profiling tools available.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Read Material Stats and Platform Stats
&lt;/h3&gt;

&lt;p&gt;Material Stats provide a quick starting point for instruction-count comparisons. Platform Stats show results for the selected rendering targets. Compare the same rendering API and shader model. An SM5 count and an SM6 count are not automatically comparable because the compilation path and reported metrics can differ. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-material-editor-ui" rel="noopener noreferrer"&gt;Material Editor UI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Treat these statistics as diagnostic evidence, not as a stopwatch. They do not capture screen coverage, all overdraw, memory-bandwidth pressure, or the effect on the rest of the frame.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use Shader Complexity and Quad Overdraw as clues
&lt;/h3&gt;

&lt;p&gt;Shader Complexity helps locate expensive regions, particularly translucent effects. Epic explicitly notes that instruction counts do not assign equivalent execution cost to every operation: texture fetches and simple arithmetic can have very different costs. Loops that are not unrolled can also be poorly represented by the visualization. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/viewport-modes-in-unreal-engine" rel="noopener noreferrer"&gt;Viewport modes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A useful sequence is to find candidates with Shader Complexity, inspect overlap with Quad Overdraw, measure the affected pass with &lt;code&gt;ProfileGPU&lt;/code&gt;, and A/B test the material under identical conditions. Use a GPU debugger such as RenderDoc or PIX when you need to inspect individual events or shader behavior more closely. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/viewport-modes-in-unreal-engine" rel="noopener noreferrer"&gt;Viewport modes&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/guidelines-for-optimizing-rendering-for-real-time-in-unreal-engine" rel="noopener noreferrer"&gt;Rendering optimization&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/shader-debugging-workflows-unreal-engine" rel="noopener noreferrer"&gt;Shader debugging&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A red region is a reason to investigate, not an instruction to rewrite the material in HLSL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Material optimizations worth investigating first
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Check blend mode, screen coverage, and overdraw
&lt;/h3&gt;

&lt;p&gt;Opaque rendering can often use depth rejection effectively. Translucency processes overlapping layers, and masked materials still need to evaluate their cutout. Several layers of foliage, fences, or large particle cards can therefore become expensive even when each individual material looks modest. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/guidelines-for-optimizing-rendering-for-real-time-in-unreal-engine" rel="noopener noreferrer"&gt;Rendering optimization&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/viewport-modes-in-unreal-engine" rel="noopener noreferrer"&gt;Viewport modes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before changing the math, ask whether the effect needs translucency, whether its polygon covers large transparent areas, whether too many particles overlap, whether distant LODs can drop layers, and whether several full-screen effects could be reduced or combined.&lt;/p&gt;

&lt;p&gt;Reducing the number of shaded pixels or overlapping layers can matter more than removing a few arithmetic instructions. Measure both options instead of assuming that the shorter formula wins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Evaluate texture access and arithmetic separately
&lt;/h3&gt;

&lt;p&gt;Neither “replace textures with math” nor “bake all math into textures” is a universal rule.&lt;/p&gt;

&lt;p&gt;Complex, static, multi-octave noise evaluated per pixel is a candidate for baking. A simple gradient may not justify adding a large texture, streaming requirements, and another sample. Consider sample count, mip selection, resolution, compression, locality, memory traffic, and whether the target workload is limited by arithmetic or bandwidth. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/guidelines-for-optimizing-rendering-for-real-time-in-unreal-engine" rel="noopener noreferrer"&gt;Rendering optimization&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Channel packing can reduce separate texture accesses when channels are used together, but the grouping matters. Do not assume that Base Color and scalar masks such as roughness, metallic, and ambient occlusion can share arbitrary compression and color-space settings. Preserve the intended data interpretation, and check whether unrelated channels are being fetched unnecessarily. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/using-texture-masks-in-unreal-engine" rel="noopener noreferrer"&gt;Texture masks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The decision belongs to the measured workload, not to the appearance of the material graph.&lt;/p&gt;

&lt;h3&gt;
  
  
  Move suitable UV calculations to the vertex stage
&lt;/h3&gt;

&lt;p&gt;Customized UVs let you evaluate UV operations in the vertex shader and interpolate the results for the pixel shader. This can help when there are substantially fewer vertices than shaded pixels. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/customized-uvs-in-unreal-engine-materials" rel="noopener noreferrer"&gt;Customized UVs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An affine operation is a typical candidate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UV * Tiling + Offset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nonlinear operations such as &lt;code&gt;sin&lt;/code&gt;, &lt;code&gt;cos&lt;/code&gt;, &lt;code&gt;length&lt;/code&gt;, or squaring generally do not survive interpolation unchanged. Moving them to the vertex stage can make the result depend on mesh density. The cost of extra interpolants also matters. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/customized-uvs-in-unreal-engine-materials" rel="noopener noreferrer"&gt;Customized UVs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do not assume that the vertex stage is always cheaper, particularly for dense geometry or a Nanite rendering path. Compare the actual target path, both visually and in GPU time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Material Instances do not automatically remove instructions
&lt;/h3&gt;

&lt;p&gt;Changing Scalar, Vector, or Texture Parameters through a Material Instance or Material Instance Dynamic (MID) normally retains the parent's shader logic. The advantage is reuse and controlled variation, not automatic removal of GPU work. Static Switch Parameters are different: they select compiled variations that can omit unused branches. &lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/instanced-materials-in-unreal-engine" rel="noopener noreferrer"&gt;Material instances&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/material-parameter-expressions-in-unreal-engine" rel="noopener noreferrer"&gt;Material parameters&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a design rule, use runtime parameters for colors, strengths, thresholds, and animation speeds. Consider a static switch when removing an entire feature branch is valuable. Separate genuinely different material families instead of building one universal parent, and inspect real static-override combinations with Material Analyzer. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-material-analyzer-tool" rel="noopener noreferrer"&gt;Material Analyzer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also review project-level rendering features. Settings for features such as stationary skylights, low-quality lightmaps, whole-scene point-light shadows, or path tracing can affect which material shaders must be compiled. Disable support only after establishing that the project does not need it. Fewer permutations primarily reduce compilation and storage; they do not guarantee a cheaper visible pixel shader. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/rendering-settings-in-the-unreal-engine-project-settings" rel="noopener noreferrer"&gt;Rendering settings&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Material Functions to improve maintainability
&lt;/h3&gt;

&lt;p&gt;Material Functions organize and reuse graph logic. They are not a promise that the GPU executes a shared computation once for every caller. Evaluate the resulting compiled material. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/material-functions-in-unreal-engine" rel="noopener noreferrer"&gt;Material Functions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A practical boundary is to separate responsibilities with Material Functions, then introduce Custom HLSL only where standard nodes are insufficient or the equivalent graph becomes difficult to maintain. This keeps more of the workflow accessible to artists and preserves graph previews and material integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspect Substrate statistics and the GBuffer format
&lt;/h3&gt;

&lt;p&gt;Substrate represents materials using Slabs and can simplify their composition to fit project and platform budgets. Use the Material Editor's Substrate statistics to inspect topology, enabled features, and simplification. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/overview-of-substrate-materials-in-unreal-engine" rel="noopener noreferrer"&gt;Substrate overview&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Blendable GBuffer is the default for most new Substrate-enabled projects and prioritizes predictable performance. Some templates, including Automotive and Architectural, choose Adaptive GBuffer instead. Adaptive supports richer materials but increases cost and cooking work. Compare the same GBuffer format and closure configuration, not merely whether Substrate is enabled. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/overview-of-substrate-materials-in-unreal-engine" rel="noopener noreferrer"&gt;Substrate overview&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What “writing a shader in code” means in UE5
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Custom Material Expression
&lt;/h3&gt;

&lt;p&gt;A Custom expression replaces part of a material graph with HLSL. Unreal still handles material integration, parameters, lighting, and the relevant rendering passes. It is usually the smallest step beyond ordinary graph authoring. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/custom-material-expressions-in-unreal-engine" rel="noopener noreferrer"&gt;Custom expressions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Typical reasons to use it include a missing formula, a readable fixed-count loop, bit operations, or a calculation that becomes unwieldy as nodes.&lt;/p&gt;

&lt;p&gt;Keep the boundaries clear. Unreal cannot perform all of its material-level constant folding inside a Custom expression. Inputs are function-local because the code is wrapped in a generated function. Valid HLSL can still encounter backend-specific limitations. None of this means that the downstream shader compiler performs no optimization. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/custom-material-expressions-in-unreal-engine" rel="noopener noreferrer"&gt;Custom expressions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;World-space calculations also need care. UE5's Large World Coordinates (LWC) rendering types and translated world space exist to manage precision. Blindly reducing large coordinates to &lt;code&gt;float3&lt;/code&gt; can introduce errors far from the origin. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/large-world-coordinates-rendering-in-unreal-engine-5" rel="noopener noreferrer"&gt;LWC rendering&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A Custom expression remains part of a material. It does not give you arbitrary render-pass scheduling or unrestricted UAV access. Choose it for a concrete functionality, readability, or reuse benefit—not because code looks inherently faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;.ush&lt;/code&gt;, &lt;code&gt;.usf&lt;/code&gt;, and Global Shaders
&lt;/h3&gt;

&lt;p&gt;For a Global Shader, you place HLSL in shader files and register a shader type from C++. Global Shaders are not tied to a particular material or mesh and are used for tasks such as full-screen processing, compute work, and clears. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/adding-global-shaders-to-unreal-engine" rel="noopener noreferrer"&gt;Adding Global Shaders&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/shader-development-in-unreal-engine" rel="noopener noreferrer"&gt;Shader development&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The division of responsibility is important:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HLSL&lt;/td&gt;
&lt;td&gt;Vertex, pixel, or compute operations executed by the GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C++&lt;/td&gt;
&lt;td&gt;Shader registration, compile conditions, parameters, resources, passes, and dispatch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render Dependency Graph (RDG)&lt;/td&gt;
&lt;td&gt;Resource dependencies and lifetimes, transitions, scheduling, and unused-pass culling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;C++ configures the GPU work; it does not become the per-pixel GPU program. RDG manages work whose resource usage is declared to the graph. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/render-dependency-graph-in-unreal-engine" rel="noopener noreferrer"&gt;Render Dependency Graph&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Mesh Material Shaders and custom mesh passes
&lt;/h3&gt;

&lt;p&gt;Rendering meshes in a custom pass while using their materials, consuming vertex-factory data, or integrating closely with the base pass requires more than a stand-alone Global Shader. This territory includes Mesh Material Shaders, mesh pass processors, renderer integration, and suitable extension points such as Scene View Extensions. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/shader-development-in-unreal-engine" rel="noopener noreferrer"&gt;Shader development&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/mesh-drawing-pipeline-in-unreal-engine" rel="noopener noreferrer"&gt;Mesh drawing pipeline&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The integration has a larger maintenance cost across engine upgrades. For an ordinary screen effect, first consider a Post Process Material. For independent compute work, first consider a Global Shader with RDG.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom HLSL example: a rounded-rectangle mask
&lt;/h2&gt;

&lt;p&gt;This example creates a rounded rectangle centered at &lt;code&gt;UV = (0.5, 0.5)&lt;/code&gt;. &lt;strong&gt;All dimensions use ordinary UV units&lt;/strong&gt;, not a coordinate system scaled to &lt;code&gt;-1..1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Configure the Custom expression inputs as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UV&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;float2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Coordinates in the &lt;code&gt;0..1&lt;/code&gt; UV domain&lt;/td&gt;
&lt;td&gt;TextureCoordinate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HalfSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;float2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Half-width and half-height from the center&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(0.40, 0.25)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Radius&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Corner radius, in the same UV units&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.08&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Feather&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimum edge-transition width, in UV units&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.002&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Set &lt;strong&gt;Output Type&lt;/strong&gt; to &lt;code&gt;CMOT Float 1&lt;/code&gt;. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/custom-material-expressions-in-unreal-engine" rel="noopener noreferrer"&gt;Custom expression setup&lt;/a&gt;&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="kt"&gt;float2&lt;/span&gt; &lt;span class="n"&gt;safeHalfSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HalfSize&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="kt"&gt;float2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1e-5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e-5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="kt"&gt;float2&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="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="p"&gt;));&lt;/span&gt;

&lt;span class="n"&gt;float&lt;/span&gt; &lt;span class="n"&gt;safeRadius&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Radius&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="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;safeHalfSize&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;safeHalfSize&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="kt"&gt;float2&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UV&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;span class="kt"&gt;float2&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;safeHalfSize&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;safeRadius&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;distanceToEdge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nb"&gt;length&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;q&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="o"&gt;+&lt;/span&gt;
    &lt;span class="nb"&gt;min&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;q&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;q&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="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="o"&gt;-&lt;/span&gt;
    &lt;span class="n"&gt;safeRadius&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;aa&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;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Feather&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mf"&gt;1e-5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nb"&gt;fwidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;distanceToEdge&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;saturate&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="n"&gt;distanceToEdge&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;aa&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;HalfSize = (0.40, 0.25)&lt;/code&gt;, the rectangle has a &lt;strong&gt;width of 0.80 and a height of 0.50&lt;/strong&gt;. Those dimensions describe the zero-distance contour, where the returned mask is &lt;code&gt;0.5&lt;/code&gt;; the feathered transition extends to either side of that contour.&lt;/p&gt;

&lt;p&gt;The accepted inputs are finite numbers. Each half-extent is converted to its absolute value and clamped to &lt;code&gt;1e-5..0.5&lt;/code&gt;. The radius is clamped to &lt;code&gt;0..min(safeHalfSize.x, safeHalfSize.y)&lt;/code&gt;, so a negative radius becomes zero and an oversized radius cannot exceed the smaller sanitized half-extent. A negative &lt;code&gt;Feather&lt;/code&gt; uses its magnitude, with a numerical floor and screen-space derivatives determining the final transition width.&lt;/p&gt;

&lt;p&gt;Use this example in the &lt;strong&gt;pixel-shader path&lt;/strong&gt; because it uses &lt;code&gt;fwidth&lt;/code&gt;. Connect the result to an appropriate blend factor or Opacity Mask. A masked material still applies its clipping threshold; producing a smooth scalar does not by itself make masked rendering behave like alpha blending. Apply aspect correction when a circular corner in UV space would otherwise appear stretched on screen.&lt;/p&gt;

&lt;p&gt;This example is not a speed claim. An equivalent graph can be built from standard nodes. Compare the graph version and Custom HLSL version using the same Platform Stats target, generated HLSL, and GPU profiling conditions. The Custom version may be useful because it is easier to maintain, even when it is not faster.&lt;/p&gt;

&lt;p&gt;For shared code, Custom expressions expose &lt;strong&gt;Include File Paths&lt;/strong&gt; and &lt;strong&gt;Additional Defines&lt;/strong&gt;. Mapping a plugin shader directory to a virtual path lets you keep reusable functions in a &lt;code&gt;.ush&lt;/code&gt; file. Avoid putting unrelated code into one universal include: changes to a widely included file can trigger a much broader recompile. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/custom-material-expressions-in-unreal-engine" rel="noopener noreferrer"&gt;Custom expressions&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/shader-development-in-unreal-engine" rel="noopener noreferrer"&gt;Shader development&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a Global Compute Shader through a plugin
&lt;/h2&gt;

&lt;p&gt;The following example shows the &lt;strong&gt;minimal pass implementation&lt;/strong&gt; for writing a UV gradient into an RDG texture. In a game, add it from a render-thread integration point that already receives an &lt;code&gt;FRDGBuilder&lt;/code&gt;, such as an appropriate Scene View Extension callback or another renderer extension. It is not a game-thread RHI example. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/adding-global-shaders-to-unreal-engine" rel="noopener noreferrer"&gt;Adding Global Shaders&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/render-dependency-graph-in-unreal-engine" rel="noopener noreferrer"&gt;Render Dependency Graph&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Validation and integration limits:&lt;/strong&gt; These snippets illustrate shader registration, RDG resources, and dispatch. They do not form a complete screen effect. You must supply the rendering callback and a downstream consumer that composites the texture or transfers the result to an appropriate external render target. The C++ and HLSL have &lt;strong&gt;not been compiled or executed on UE 5.8.2 in this authoring environment&lt;/strong&gt;. Compilation on your engine branch and execution on each target RHI remain required checks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Folder layout
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plugins/ShaderLab/
├─ ShaderLab.uplugin
├─ Shaders/
│  └─ Private/
│     └─ WriteGradient.usf
└─ Source/
   └─ ShaderLab/
      ├─ ShaderLab.Build.cs
      ├─ Public/
      │  └─ WriteGradientShader.h
      └─ Private/
         ├─ ShaderLabModule.cpp
         └─ WriteGradientShader.cpp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load the module at &lt;code&gt;PostConfigInit&lt;/code&gt; so shader registration and the virtual shader-path mapping are available early enough. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/adding-global-shaders-to-unreal-engine" rel="noopener noreferrer"&gt;Adding Global Shaders&lt;/a&gt;&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;"FileVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"VersionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"FriendlyName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ShaderLab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CanContainContent"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Modules"&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="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;"ShaderLab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Runtime"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"LoadingPhase"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PostConfigInit"&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;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;h3&gt;
  
  
  Build.cs
&lt;/h3&gt;

&lt;p&gt;The public pass header exposes RDG types, so &lt;code&gt;RenderCore&lt;/code&gt; is a public dependency. The other dependencies below support the plugin implementation. Public include visibility and exported binary symbols are separate concerns; both will matter when another module calls the pass function. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-modules" rel="noopener noreferrer"&gt;Unreal modules&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/module-api-specifiers-in-unreal-engine" rel="noopener noreferrer"&gt;Module API specifiers&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;UnrealBuildTool&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;class&lt;/span&gt; &lt;span class="nc"&gt;ShaderLab&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ModuleRules&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;ShaderLab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReadOnlyTargetRules&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="k"&gt;base&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="n"&gt;PCHUsage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PCHUsageMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseExplicitOrSharedPCHs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;PublicDependencyModuleNames&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="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;"RenderCore"&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&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="p"&gt;[]&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;"Projects"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"Renderer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"RHI"&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Map the shader directory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ShaderLabModule.cpp&lt;/span&gt;

&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"Interfaces/IPluginManager.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Misc/Paths.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Modules/ModuleManager.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"ShaderCore.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;FShaderLabModule&lt;/span&gt; &lt;span class="k"&gt;final&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;IModuleInterface&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;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;StartupModule&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;const&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;IPlugin&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Plugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;IPluginManager&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;FindPlugin&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;"ShaderLab"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Plugin&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;const&lt;/span&gt; &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;ShaderDirectory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;FPaths&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Plugin&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetBaseDir&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;"Shaders"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;AddShaderSourceDirectoryMapping&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;"/ShaderLab"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;ShaderDirectory&lt;/span&gt;&lt;span class="p"&gt;);&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;ShutdownModule&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;IMPLEMENT_MODULE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FShaderLabModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ShaderLab&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The physical directory &lt;code&gt;Plugins/ShaderLab/Shaders&lt;/code&gt; can now be addressed through the virtual shader path &lt;code&gt;/ShaderLab&lt;/code&gt;. The registration below uses that virtual path, not a machine-specific filesystem path. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/overview-of-shaders-in-plugins-unreal-engine" rel="noopener noreferrer"&gt;Shaders in plugins&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Write the HLSL
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hlsl"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Shaders/Private/WriteGradient.usf&lt;/span&gt;

&lt;span class="kt"&gt;RWTexture2D&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OutputTexture&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;int2&lt;/span&gt; &lt;span class="n"&gt;OutputSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;numthreads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&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="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;MainCS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uint3&lt;/span&gt; &lt;span class="n"&gt;DispatchThreadId&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SV_DispatchThreadID&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;DispatchThreadId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&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;uint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;OutputSize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="n"&gt;DispatchThreadId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&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;uint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;OutputSize&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="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="kt"&gt;float2&lt;/span&gt; &lt;span class="n"&gt;uv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DispatchThreadId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xy&lt;/span&gt;&lt;span class="p"&gt;)&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;span class="o"&gt;/&lt;/span&gt;
        &lt;span class="kt"&gt;float2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OutputSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;OutputTexture&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DispatchThreadId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="kt"&gt;float4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uv&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="mi"&gt;1&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;Each thread writes one texel. The half-texel offset evaluates the gradient at texel centers. The bounds check protects the final thread groups when the texture dimensions are not multiples of eight. The caller must supply a texture with positive dimensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Register the Global Shader and add the RDG pass
&lt;/h3&gt;

&lt;p&gt;A public function called from another module needs the export macro &lt;strong&gt;&lt;code&gt;SHADERLAB_API&lt;/code&gt;&lt;/strong&gt;. Putting a header in &lt;code&gt;Public&lt;/code&gt; makes it available for inclusion; that alone does not export the function across a modular-build DLL boundary. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/module-api-specifiers-in-unreal-engine" rel="noopener noreferrer"&gt;Module API specifiers&lt;/a&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="c1"&gt;// Public/WriteGradientShader.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;"RenderGraphFwd.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="n"&gt;SHADERLAB_API&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AddWriteGradientPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;FRDGBuilder&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;GraphBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;FRDGTextureRef&lt;/span&gt; &lt;span class="n"&gt;OutputTexture&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 consumer calls the function only from its &lt;code&gt;.cpp&lt;/code&gt; implementation, add the dependency in that consumer module's &lt;code&gt;Build.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;PrivateDependencyModuleNames&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="s"&gt;"ShaderLab"&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;PublicDependencyModuleNames&lt;/code&gt; instead when the consumer's public interface itself requires the ShaderLab header. Likewise, expose the appropriate owning-module dependencies for any engine types used by that public interface. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-modules" rel="noopener noreferrer"&gt;Unreal modules&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The implementation registers the shader and schedules its work:&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;// WriteGradientShader.cpp&lt;/span&gt;

&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"WriteGradientShader.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"DataDrivenShaderPlatformInfo.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"GlobalShader.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"RenderGraphBuilder.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"RenderGraphUtils.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"ShaderParameterStruct.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;FWriteGradientCS&lt;/span&gt; &lt;span class="k"&gt;final&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;FGlobalShader&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;DECLARE_GLOBAL_SHADER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FWriteGradientCS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;SHADER_USE_PARAMETER_STRUCT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FWriteGradientCS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FGlobalShader&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;BEGIN_SHADER_PARAMETER_STRUCT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FParameters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;SHADER_PARAMETER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FIntPoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OutputSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;SHADER_PARAMETER_RDG_TEXTURE_UAV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;RWTexture2D&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;float4&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;OutputTexture&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;END_SHADER_PARAMETER_STRUCT&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;ShouldCompilePermutation&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;FGlobalShaderPermutationParameters&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;return&lt;/span&gt; &lt;span class="n"&gt;IsFeatureLevelSupported&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;Platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ERHIFeatureLevel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SM5&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;IMPLEMENT_GLOBAL_SHADER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;FWriteGradientCS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"/ShaderLab/Private/WriteGradient.usf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"MainCS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SF_Compute&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AddWriteGradientPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;FRDGBuilder&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;GraphBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;FRDGTextureRef&lt;/span&gt; &lt;span class="n"&gt;OutputTexture&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OutputTexture&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;FIntPoint&lt;/span&gt; &lt;span class="n"&gt;OutputSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OutputTexture&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Desc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Extent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;FWriteGradientCS&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FParameters&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Parameters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;GraphBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllocParameters&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
            &lt;span class="n"&gt;FWriteGradientCS&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FParameters&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;Parameters&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;OutputSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OutputSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;OutputTexture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;GraphBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateUAV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;FRDGTextureUAVDesc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OutputTexture&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="n"&gt;TShaderMapRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FWriteGradientCS&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ComputeShader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;GetGlobalShaderMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GMaxRHIFeatureLevel&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="n"&gt;FComputeShaderUtils&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AddPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;GraphBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;RDG_EVENT_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ShaderLab.WriteGradient"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;ComputeShader&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;FComputeShaderUtils&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GetGroupCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;OutputSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;FIntPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&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 &lt;code&gt;SM5&lt;/code&gt; feature-level check is a compile filter, not a claim that every admitted platform has been tested. The intended DX12/SM6 target is not made SM5-only by this minimum-feature-level check. Keep the shader map and compile policy consistent with the rendering path used by your integration. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/shader-development-in-unreal-engine" rel="noopener noreferrer"&gt;Shader development&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output texture must be created by, or registered with, &lt;strong&gt;the same &lt;code&gt;FRDGBuilder&lt;/code&gt;&lt;/strong&gt; and support the required UAV usage. This example expects a non-multisampled 2D texture at mip zero, with a format compatible with its &lt;code&gt;float4&lt;/code&gt; UAV. It does not handle texture arrays or arbitrary mip views. The following caller-side fragment assumes an existing graph and a positive &lt;code&gt;FIntPoint OutputSize&lt;/code&gt;; it is not a complete rendering callback:&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="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FRDGTextureDesc&lt;/span&gt; &lt;span class="n"&gt;OutputDesc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;FRDGTextureDesc&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Create2D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;OutputSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;PF_FloatRGBA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;FClearValueBinding&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Black&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;TexCreate_ShaderResource&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;TexCreate_UAV&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;FRDGTextureRef&lt;/span&gt; &lt;span class="n"&gt;OutputTexture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;GraphBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateTexture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;OutputDesc&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;"ShaderLab.GradientOutput"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="n"&gt;AddWriteGradientPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GraphBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OutputTexture&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within the graph, another pass can read the result as an SRV and composite it. Alternatively, use the appropriate RDG extraction or external-resource path, such as &lt;code&gt;QueueTextureExtraction&lt;/code&gt;, to retain the underlying resource after graph execution. Do not retain the &lt;code&gt;FRDGTextureRef&lt;/code&gt; itself beyond its graph lifetime. A pass with no live output consumer may be culled; allocating a texture is not the same as displaying it. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/render-dependency-graph-in-unreal-engine" rel="noopener noreferrer"&gt;Render Dependency Graph&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Splitting the caller into another class or module does not change those lifetime rules. Keep the builder and RDG handles within the graph's lifetime. Do not capture game-thread &lt;code&gt;UObject&lt;/code&gt; state or raw pointers without a valid render-thread ownership and synchronization design. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/render-dependency-graph-in-unreal-engine" rel="noopener noreferrer"&gt;Render Dependency Graph&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What to optimize in this example
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The five-argument &lt;code&gt;FComputeShaderUtils::AddPass&lt;/code&gt; above schedules an ordinary compute pass on the graphics pipe.&lt;/strong&gt; Writing a compute shader does not automatically run it on the Async Compute pipe or overlap it with graphics work. The pass-flag overload is separate, and &lt;code&gt;ERDGPassFlags::Compute&lt;/code&gt; and &lt;code&gt;ERDGPassFlags::AsyncCompute&lt;/code&gt; describe different execution pipes. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/RenderCore/FComputeShaderUtils__AddPass" rel="noopener noreferrer"&gt;AddPass API&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/RenderCore/ERDGPassFlags" rel="noopener noreferrer"&gt;Pass flags&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For this pass, investigate group size, texture format, UAV traffic, unnecessary dispatch area, graph dependencies and lifetimes, permitted shader permutations, and first-use PSO behavior. An &lt;code&gt;8 x 8&lt;/code&gt; group is a starting point, not a universal optimum. Register use, shared memory, wave size, and access patterns can change the result. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/render-dependency-graph-in-unreal-engine" rel="noopener noreferrer"&gt;Render Dependency Graph&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/shader-development-in-unreal-engine" rel="noopener noreferrer"&gt;Shader development&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Only when testing Async Compute as an &lt;strong&gt;optional A/B variant&lt;/strong&gt;, replace the shown call with the overload that accepts pass flags and specify &lt;code&gt;ERDGPassFlags::AsyncCompute&lt;/code&gt;. That is a candidate to measure, not a default improvement. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/RenderCore/FComputeShaderUtils__AddPass" rel="noopener noreferrer"&gt;AddPass API&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An async flag does not guarantee useful overlap. Resource dependencies, queue synchronization, RHI and hardware support, and competition for compute or memory bandwidth can eliminate the benefit or make the frame slower. Inspect the actual schedule and overlap with RDG Insights and GPU profiling tools. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/render-dependency-graph-in-unreal-engine" rel="noopener noreferrer"&gt;Render Dependency Graph&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide between a Custom expression and a Global Shader
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Stay with a Custom expression when the material model fits
&lt;/h3&gt;

&lt;p&gt;A Custom expression is a good fit when you want to compute material inputs such as Base Color, Normal, Roughness, or Opacity Mask; keep Unreal's normal material and mesh-pass integration; express a formula more clearly; and continue controlling values through Material Instances. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/custom-material-expressions-in-unreal-engine" rel="noopener noreferrer"&gt;Custom expressions&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/material-parameter-expressions-in-unreal-engine" rel="noopener noreferrer"&gt;Material parameters&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You do not need a custom rendering pass merely because a material contains HLSL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use a Global Shader when you need independent GPU work
&lt;/h3&gt;

&lt;p&gt;Move to a Global Shader when the operation needs writes to resources such as &lt;code&gt;RWTexture2D&lt;/code&gt; or &lt;code&gt;RWBuffer&lt;/code&gt;, arbitrary compute dispatch dimensions, a material-independent full-screen operation, or explicit RDG dependencies between intermediate textures and buffers. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/adding-global-shaders-to-unreal-engine" rel="noopener noreferrer"&gt;Adding Global Shaders&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/render-dependency-graph-in-unreal-engine" rel="noopener noreferrer"&gt;Render Dependency Graph&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The benefit is control over GPU work and resources, not an automatic reduction in execution time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extend the renderer when the mesh pipeline itself must change
&lt;/h3&gt;

&lt;p&gt;Custom mesh drawing, vertex-factory-specific data, deep base-pass or shadow integration, Nanite integration, new shading models, and new GBuffer representations belong to a more tightly coupled layer. Some changes require engine modifications rather than a self-contained plugin. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/mesh-drawing-pipeline-in-unreal-engine" rel="noopener noreferrer"&gt;Mesh drawing pipeline&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/shader-development-in-unreal-engine" rel="noopener noreferrer"&gt;Shader development&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Budget for engine-upgrade maintenance, validation on every target RHI, and pipeline-cache coverage. Do not choose this layer merely to make an ordinary material graph look shorter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design permutations and PSOs deliberately
&lt;/h2&gt;

&lt;p&gt;The same permutation discipline applies to code-authored shaders and materials.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;ShouldCompilePermutation&lt;/code&gt; to exclude unsupported or unnecessary platforms. When using a permutation domain, avoid generating combinations that runtime code cannot reach. A runtime parameter can be preferable to another compile-time define when the variation does not justify a separate shader. Conversely, a small, controlled set of permutations may be worthwhile when it removes substantial runtime work. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/shader-development-in-unreal-engine" rel="noopener noreferrer"&gt;Shader development&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the material side, Material Analyzer groups instances with matching static overrides and helps identify candidates for a shared intermediate parent. Review the interaction of project features, Static Switches, material quality, and feature levels rather than treating each setting in isolation. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-material-analyzer-tool" rel="noopener noreferrer"&gt;Material Analyzer&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/rendering-settings-in-the-unreal-engine-project-settings" rel="noopener noreferrer"&gt;Rendering settings&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For PSOs, verify distinct milestones: the shader was cooked; the pipeline is covered by precaching; important requests finish before gameplay needs them; actual play does not reveal missed or late pipelines; and any fallback material or delayed draw behavior is acceptable. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/pso-precaching-for-unreal-engine" rel="noopener noreferrer"&gt;PSO precaching&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A smooth editor session is not proof of a smooth first launch. Test the packaged target with an appropriately clean cache state. Shader compilation and DDC behavior, pipeline precaching, and the graphics driver's cache are related but are not the same cache or the same test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recompile and debug during development
&lt;/h2&gt;

&lt;p&gt;For work on &lt;code&gt;.usf&lt;/code&gt; and &lt;code&gt;.ush&lt;/code&gt; files, enable shader development diagnostics in &lt;code&gt;ConsoleVariables.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="py"&gt;r.ShaderDevelopmentMode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This enables shader-development logging and support for retrying compilation after errors. After saving, &lt;code&gt;recompileshaders changed&lt;/code&gt; or &lt;code&gt;Ctrl+Shift+.&lt;/code&gt; recompiles changed shaders. A change to a widely included file can invalidate a large set of dependent shaders, so keep shared includes focused. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/shader-development-in-unreal-engine" rel="noopener noreferrer"&gt;Shader development&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For GPU debugging, use the platform-appropriate symbol settings, including &lt;code&gt;r.Shaders.Symbols&lt;/code&gt; and &lt;code&gt;r.Shaders.WriteSymbols&lt;/code&gt; where supported by your workflow. Symbol generation affects cooking time and storage. Enable it deliberately for the platforms and investigations that need it rather than assuming it is free. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/shader-debugging-workflows-unreal-engine" rel="noopener noreferrer"&gt;Shader debugging workflows&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical optimization checklist
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Before changing anything
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Fix the target hardware, RHI, shader model, and resolution.&lt;/li&gt;
&lt;li&gt;[ ] Express the frame-rate target as a time budget in milliseconds.&lt;/li&gt;
&lt;li&gt;[ ] Use &lt;code&gt;stat unit&lt;/code&gt; and GPU profiling to establish the bottleneck.&lt;/li&gt;
&lt;li&gt;[ ] Identify the affected pass with &lt;code&gt;ProfileGPU&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Save the comparison scene, camera, quality settings, and warm-up conditions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When changing a material
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Check blend mode, screen coverage, and overdraw before editing the formula.&lt;/li&gt;
&lt;li&gt;[ ] Measure GPU time; do not rely on Shader Complexity alone.&lt;/li&gt;
&lt;li&gt;[ ] Evaluate texture samples, arithmetic, and affected pixel count separately.&lt;/li&gt;
&lt;li&gt;[ ] Test whether suitable UV calculations can move to Customized UVs without changing the image.&lt;/li&gt;
&lt;li&gt;[ ] Estimate permutation growth before adding a Static Switch.&lt;/li&gt;
&lt;li&gt;[ ] Use Material Instances and Material Functions for controlled reuse and maintainability.&lt;/li&gt;
&lt;li&gt;[ ] Compare Custom HLSL against equivalent standard nodes on the same target.&lt;/li&gt;
&lt;li&gt;[ ] For Substrate, compare statistics, simplification, and GBuffer format.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When adding a Global Shader
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Explain why a Material or Post Process Material cannot meet the requirement.&lt;/li&gt;
&lt;li&gt;[ ] Load shader registration and virtual-path mapping early enough.&lt;/li&gt;
&lt;li&gt;[ ] Match C++ parameter names and types with the HLSL declarations.&lt;/li&gt;
&lt;li&gt;[ ] Export cross-module functions or types with the correct &lt;code&gt;MODULENAME_API&lt;/code&gt; macro.&lt;/li&gt;
&lt;li&gt;[ ] Declare consumer module dependencies at the correct public/private boundary.&lt;/li&gt;
&lt;li&gt;[ ] Keep RDG textures and buffers within the lifetime of their graph.&lt;/li&gt;
&lt;li&gt;[ ] Match resource usage and format to the required UAV, SRV, or render-target access.&lt;/li&gt;
&lt;li&gt;[ ] Check dispatch bounds and use positive resource dimensions.&lt;/li&gt;
&lt;li&gt;[ ] Restrict shader compilation with &lt;code&gt;ShouldCompilePermutation&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Name GPU events so the pass can be identified in profiling tools.&lt;/li&gt;
&lt;li&gt;[ ] Distinguish ordinary compute from Async Compute and measure actual overlap for the async variant.&lt;/li&gt;
&lt;li&gt;[ ] Verify PSO coverage and first use with clean caches.&lt;/li&gt;
&lt;li&gt;[ ] Compile, inspect the output, and measure performance on every target RHI and device.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A useful order of investigation is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure → reduce shaded area and overdraw → simplify material structure → use Custom HLSL where justified → add Global Shaders or renderer extensions when the requirements demand them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Material graphs already become HLSL. Moving work into HLSL or C++ does not automatically make it faster. Start with screen coverage, translucency, texture access, unused features, and unnecessary static variations. &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/essential-unreal-engine-material-concepts" rel="noopener noreferrer"&gt;Material concepts&lt;/a&gt; · &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/guidelines-for-optimizing-rendering-for-real-time-in-unreal-engine" rel="noopener noreferrer"&gt;Rendering optimization&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use Custom expressions for material-local formulas that benefit from code. Use Global Shaders and RDG for independent resources and dispatch. Use mesh-pipeline integration when the rendering architecture actually needs to change.&lt;/p&gt;

&lt;p&gt;The right layer is not the lowest-level one. It is &lt;strong&gt;the simplest layer that meets the requirement, can be measured on the target platform, and remains maintainable by the team&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;The following links point to Epic's primary documentation and release announcement. Version-dependent behavior should be checked against the engine branch used by the project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forums.unrealengine.com/t/5-8-2-hotfix-released/2746335" rel="noopener noreferrer"&gt;Unreal Engine 5.8.2 Hotfix Released&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/essential-unreal-engine-material-concepts" rel="noopener noreferrer"&gt;Essential Unreal Engine Material Concepts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-material-editor-ui" rel="noopener noreferrer"&gt;Unreal Engine Material Editor UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/custom-material-expressions-in-unreal-engine" rel="noopener noreferrer"&gt;Custom Material Expressions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/material-parameter-expressions-in-unreal-engine" rel="noopener noreferrer"&gt;Material Parameter Expressions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/instanced-materials-in-unreal-engine" rel="noopener noreferrer"&gt;Material Instances&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/material-functions-in-unreal-engine" rel="noopener noreferrer"&gt;Material Functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/using-texture-masks-in-unreal-engine" rel="noopener noreferrer"&gt;Using Texture Masks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/customized-uvs-in-unreal-engine-materials" rel="noopener noreferrer"&gt;Customized UVs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-material-analyzer-tool" rel="noopener noreferrer"&gt;Material Analyzer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/viewport-modes-in-unreal-engine" rel="noopener noreferrer"&gt;Viewport Modes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/guidelines-for-optimizing-rendering-for-real-time-in-unreal-engine" rel="noopener noreferrer"&gt;Guidelines for Optimizing Rendering for Real-Time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/rendering-settings-in-the-unreal-engine-project-settings" rel="noopener noreferrer"&gt;Rendering Project Settings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/timing-insights-in-unreal-engine" rel="noopener noreferrer"&gt;Timing Insights&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/shader-development-in-unreal-engine" rel="noopener noreferrer"&gt;Shader Development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/adding-global-shaders-to-unreal-engine" rel="noopener noreferrer"&gt;Adding Global Shaders&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/module-api-specifiers-in-unreal-engine" rel="noopener noreferrer"&gt;Module API Specifiers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-modules" rel="noopener noreferrer"&gt;Unreal Engine Modules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/overview-of-shaders-in-plugins-unreal-engine" rel="noopener noreferrer"&gt;Overview of Shaders in Plugins&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/RenderCore/FComputeShaderUtils__AddPass" rel="noopener noreferrer"&gt;FComputeShaderUtils::AddPass&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/RenderCore/ERDGPassFlags" rel="noopener noreferrer"&gt;ERDGPassFlags&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/render-dependency-graph-in-unreal-engine" rel="noopener noreferrer"&gt;Render Dependency Graph&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/mesh-drawing-pipeline-in-unreal-engine" rel="noopener noreferrer"&gt;Mesh Drawing Pipeline&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/pso-precaching-for-unreal-engine" rel="noopener noreferrer"&gt;PSO Precaching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/shader-debugging-workflows-unreal-engine" rel="noopener noreferrer"&gt;Shader Debugging Workflows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/large-world-coordinates-rendering-in-unreal-engine-5" rel="noopener noreferrer"&gt;Large World Coordinates Rendering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/substrate-materials-in-unreal-engine" rel="noopener noreferrer"&gt;Substrate Materials: feature status&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/overview-of-substrate-materials-in-unreal-engine" rel="noopener noreferrer"&gt;Overview of Substrate Materials&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unrealengine</category>
      <category>gamedev</category>
      <category>cpp</category>
      <category>graphics</category>
    </item>
    <item>
      <title>Choosing UI Tools in Unreal Engine 5: UMG, CommonUI, Slate, Blueprint, and C++</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Fri, 25 Sep 2026 03:37:43 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/choosing-ui-tools-in-unreal-engine-5-umg-commonui-slate-blueprint-and-c-330n</link>
      <guid>https://dev.to/gamedevtoollab/choosing-ui-tools-in-unreal-engine-5-umg-commonui-slate-blueprint-and-c-330n</guid>
      <description>&lt;p&gt;Designing a UI in Unreal Engine 5 is not a binary choice between Widget Blueprints and C++. The more useful starting point is to separate four responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;State ownership:&lt;/strong&gt; Characters, Actor Components, PlayerStates, or Subsystems hold gameplay state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presentation:&lt;/strong&gt; A thin adapter forwards simple values; a Presenter or Viewmodel handles more complex display transformations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Widget ownership:&lt;/strong&gt; A PlayerController, HUD, or &lt;code&gt;ULocalPlayerSubsystem&lt;/code&gt;-based UI manager creates and retains screens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input routing:&lt;/strong&gt; The PlayerController, CommonUI Action Router, or UI manager coordinates input, focus, and mouse capture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guide uses &lt;strong&gt;Unreal Engine 5.8&lt;/strong&gt; as its technical baseline. It covers UMG, CommonUI, Slate, Widget Components, and UMG Viewmodel, then connects those choices to both Blueprint-first and C++-first implementations.&lt;/p&gt;

&lt;p&gt;The default approach is straightforward: start with UMG, keep layout and visual iteration in Widget Blueprints, and place state, ownership, and subscription contracts in Blueprint or C++ according to the project's needs. The important boundary is the responsibility—not the language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the use case and maturity level
&lt;/h2&gt;

&lt;p&gt;The following table describes the UE5.8 baseline. &lt;strong&gt;Standard&lt;/strong&gt; means the source documentation does not explicitly label the feature Beta or Experimental; it is not a promise that a project needs no testing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Main option&lt;/th&gt;
&lt;th&gt;Maturity and constraints&lt;/th&gt;
&lt;th&gt;Adoption guidance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HUDs, menus, and dialogs&lt;/td&gt;
&lt;td&gt;UMG&lt;/td&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;The default choice. Keep gameplay state and lifetime management outside the Widget Blueprint.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layered menus, Back navigation, and gamepad support&lt;/td&gt;
&lt;td&gt;UMG + CommonUI&lt;/td&gt;
&lt;td&gt;The release notes describe Common UI itself as production-ready; the Enhanced Input how-to retains older warnings.&lt;/td&gt;
&lt;td&gt;Distinguish the current integration path from the legacy path and test the one you use.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Many displayed fields or state shared across screens&lt;/td&gt;
&lt;td&gt;Manual events / Presenter; MVVM when needed&lt;/td&gt;
&lt;td&gt;UMG Viewmodel is Beta.&lt;/td&gt;
&lt;td&gt;Do not introduce MVVM merely because a small screen has a few values. Add regression tests when adopting it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overhead health bars, terminals, and VR panels&lt;/td&gt;
&lt;td&gt;Widget Component + UMG&lt;/td&gt;
&lt;td&gt;Standard; requires world rendering and pointer design.&lt;/td&gt;
&lt;td&gt;Measure widget count, &lt;code&gt;Draw Size&lt;/code&gt;, update frequency, occlusion, and interaction.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editor production tools&lt;/td&gt;
&lt;td&gt;Editor Utility Widget&lt;/td&gt;
&lt;td&gt;Beta&lt;/td&gt;
&lt;td&gt;Move heavy asset processing into a C++ editor module.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editor plugins and low-level UI components&lt;/td&gt;
&lt;td&gt;Slate&lt;/td&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;C++-oriented. Do not rebuild every ordinary game screen in Slate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crosshairs and immediate-mode diagnostic drawing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AHUD&lt;/code&gt; Canvas&lt;/td&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;Suitable for simple drawing and debugging, not a general menu framework.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Research and diagnostic UI&lt;/td&gt;
&lt;td&gt;UIFramework / SlateIM&lt;/td&gt;
&lt;td&gt;Experimental&lt;/td&gt;
&lt;td&gt;Not the default choice for a shipping product.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The maturity labels are documented in Epic's references for &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/editor-utility-widgets-in-unreal-engine" rel="noopener noreferrer"&gt;Editor Utility Widgets&lt;/a&gt;, &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/umg-viewmodel-for-unreal-engine" rel="noopener noreferrer"&gt;UMG Viewmodel&lt;/a&gt;, &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/UIFramework" rel="noopener noreferrer"&gt;UIFramework&lt;/a&gt;, and &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/PluginIndex/SlateIM" rel="noopener noreferrer"&gt;SlateIM&lt;/a&gt;. The CommonUI documentation discrepancy is discussed below with links to both sources.&lt;/p&gt;

&lt;p&gt;When the choice is unclear, start with UMG. Add CommonUI for screen stacks and focus, a Presenter or MVVM for complex presentation, and Slate only for low-level components that are otherwise missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the data path from the control path
&lt;/h2&gt;

&lt;p&gt;A Presenter or Viewmodel produces display data. A &lt;code&gt;ULocalPlayerSubsystem&lt;/code&gt;-based UI manager owns screens and coordinates stacks and input. These are different responsibilities, not interchangeable names for the same layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Data, simple:
  State owner --event--&amp;gt; Screen owner / thin adapter --&amp;gt; Widget

Data, complex:
  State owner --event--&amp;gt; Presenter / Viewmodel --&amp;gt; Widget

Control:
  PlayerController / HUD / LocalPlayer UI manager
    --create, retain, push, pop--&amp;gt; Widget
  CommonUI Action Router / PlayerController
    --input policy, Back, focus, mouse capture--&amp;gt; Widget
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the ammunition example later in this article, the PlayerController also acts as a minimal presentation adapter: it forwards values to the widget. Extract a dedicated Presenter or Viewmodel when formatting, multiple data sources, asynchronous state, shared presentation, or isolated tests make that forwarding layer more complex.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choose an owner by lifetime and player scope
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;th&gt;Lifetime and scope&lt;/th&gt;
&lt;th&gt;Main responsibility and when to choose it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APlayerController&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Represents one controlling player and generally outlives a Pawn. Behavior across map travel depends on the travel mechanism.&lt;/td&gt;
&lt;td&gt;A few HUD widgets, the Owning Player relationship, and reconnection after Pawn changes. Appropriate while the responsibility remains small.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AHUD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tied to the PlayerController/world-side lifetime; can span Pawn changes.&lt;/td&gt;
&lt;td&gt;HUD coordination and Canvas drawing, especially when the existing architecture already uses the HUD as its entry point.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ULocalPlayerSubsystem&lt;/code&gt;-based UI manager&lt;/td&gt;
&lt;td&gt;Shares the LocalPlayer's lifetime, outlives individual Pawns, and follows that LocalPlayer across world changes.&lt;/td&gt;
&lt;td&gt;Layers, stacks, and input coordination. Useful for multiple screens and split-screen play.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GameInstance / GameViewport-level owner&lt;/td&gt;
&lt;td&gt;Application-wide or scoped to a GameInstance rather than one local player.&lt;/td&gt;
&lt;td&gt;Loading screens, global notifications, and player-independent overlays.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actor + Widget Component&lt;/td&gt;
&lt;td&gt;Follows the Actor/world lifetime.&lt;/td&gt;
&lt;td&gt;Overhead indicators, in-world terminals, and VR panels.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;ULocalPlayerSubsystem&lt;/code&gt; shares its lifetime with &lt;code&gt;ULocalPlayer&lt;/code&gt; and exposes &lt;code&gt;PlayerControllerChanged&lt;/code&gt;. See the &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Engine/ULocalPlayerSubsystem" rel="noopener noreferrer"&gt;ULocalPlayerSubsystem API&lt;/a&gt;. A persistent manager does not make a widget's references to an old world safe to retain: recreate and reconnect world-dependent widgets when the map changes.&lt;/p&gt;

&lt;p&gt;Create player-specific UI with a valid Owning Player and add it with &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/UMG/UUserWidget/AddToPlayerScreen" rel="noopener noreferrer"&gt;&lt;code&gt;UUserWidget::AddToPlayerScreen&lt;/code&gt;&lt;/a&gt;. Decide who owns the screen separately from who owns the gameplay data shown on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing among the UI features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  UMG: the default for game screens
&lt;/h3&gt;

&lt;p&gt;UMG is the starting point for HUDs, title screens, pause menus, inventories, shops, and conversations.&lt;/p&gt;

&lt;p&gt;A Widget Blueprint should not become the owner of saving, networking, purchases, game progression, or a per-frame search for gameplay state. Keep that state elsewhere and pass the widget the data it needs to display.&lt;/p&gt;

&lt;h3&gt;
  
  
  CommonUI: layered interaction on top of UMG
&lt;/h3&gt;

&lt;p&gt;CommonUI adds input routing, Activatable Widgets, styling, and input glyphs to UMG. It is useful for switching input devices, managing menu layers, handling Back, and restoring focus. See the &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/overview-of-advanced-multiplatform-user-interfaces-with-common-ui-for-unreal-engine" rel="noopener noreferrer"&gt;Common UI Overview&lt;/a&gt;. Use Activatable Widgets for screens that participate in the interaction flow, rather than making every decorative widget activatable.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UCommonActivatableWidget&lt;/code&gt; can activate and deactivate, but it does &lt;strong&gt;not&lt;/strong&gt; automatically activate or register as a Back handler by default. Enable &lt;strong&gt;Auto Activate&lt;/strong&gt; or activate it explicitly, configure &lt;strong&gt;Back Handler&lt;/strong&gt; where appropriate, and provide a &lt;strong&gt;Desired Focus Target&lt;/strong&gt;. &lt;code&gt;RequestRefreshFocus&lt;/code&gt; restores focus only when the widget is the leaf-most active node. See the &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/CommonUI/UCommonActivatableWidget" rel="noopener noreferrer"&gt;Activatable Widget API&lt;/a&gt; and &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/CommonUI/UCommonActivatableWidget/RequestRefreshFocus" rel="noopener noreferrer"&gt;&lt;code&gt;RequestRefreshFocus&lt;/code&gt; API&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  UE5.8 CommonUI and Enhanced Input: the documentation is inconsistent
&lt;/h4&gt;

&lt;p&gt;The &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-5-8-release-notes" rel="noopener noreferrer"&gt;UE5.8 release notes&lt;/a&gt; describe the integration of Enhanced Input with Common Input/UI, a reduction in duplicated Data Assets, reliability and debugging improvements, and Common UI itself becoming production-ready.&lt;/p&gt;

&lt;p&gt;Meanwhile, the &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/using-commonui-with-enhnaced-input-in-unreal-engine" rel="noopener noreferrer"&gt;Enhanced Input integration how-to&lt;/a&gt; retains &lt;strong&gt;Experimental&lt;/strong&gt; and &lt;strong&gt;limited support&lt;/strong&gt; wording, together with a shipping warning originating from UE5.2. Do not use either page alone to make an unconditional claim about stability or shipping suitability.&lt;/p&gt;

&lt;p&gt;Keep the two configuration paths distinct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Current path:&lt;/strong&gt; Input Actions, Player Mappable Key Settings, metadata implementing &lt;code&gt;ICommonMappingContextMetadataInterface&lt;/code&gt;, and Input Mapping Contexts (IMCs). The standard metadata class is &lt;code&gt;UCommonMappingContextMetadata&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legacy path:&lt;/strong&gt; A DataTable using &lt;code&gt;CommonInputActionDataBase&lt;/code&gt; as its row structure, together with row handles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared asset does not mean legacy:&lt;/strong&gt; &lt;code&gt;CommonUIInputData&lt;/code&gt; can also reference the current Accept/Back Input Actions. Its presence alone does not identify which path a project uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration rule:&lt;/strong&gt; Choose one authoritative registration path for each action. Do not register the same action through both paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When &lt;strong&gt;Is Generic Input Action&lt;/strong&gt; is enabled, CommonUI does not fire the ordinary Enhanced Input event for that action. Also check for duplicate registration between the CommonUI layer and gameplay input. See the &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/commonui-input-technical-guide-for-unreal-engine" rel="noopener noreferrer"&gt;CommonUI Input Technical Guide&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slate, Canvas, and Widget Components
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Slate&lt;/strong&gt; is appropriate for editor plugins and low-level components that UMG does not provide. It is not a requirement merely because a project uses C++. See the &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/slate-user-interface-programming-framework-for-unreal-engine" rel="noopener noreferrer"&gt;Slate UI Framework&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;AHUD&lt;/code&gt; Canvas&lt;/strong&gt; is useful for crosshairs and diagnostic drawing. It is not a replacement for a menu architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;UWidgetComponent&lt;/code&gt;&lt;/strong&gt; requires explicit decisions about draw size, redraw policy, distance, occlusion, and how many widgets update. See &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/widget-components-in-unreal-engine" rel="noopener noreferrer"&gt;Widget Components&lt;/a&gt;. For &lt;strong&gt;&lt;code&gt;UWidgetInteractionComponent&lt;/code&gt;&lt;/strong&gt;, allocate Virtual User and Pointer indices deliberately so independent players or VR hands do not collide. See &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/umg-widget-interaction-components-in-unreal-engine" rel="noopener noreferrer"&gt;Widget Interaction Components&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Beta and Experimental features
&lt;/h3&gt;

&lt;p&gt;Editor Utility Widgets and UMG Viewmodel are &lt;strong&gt;Beta&lt;/strong&gt;. UIFramework and SlateIM are &lt;strong&gt;Experimental&lt;/strong&gt;. Keep those labels visible in the adoption decision, not just in a footnote. For editor tooling, move heavy processing into a C++ editor module; treat Experimental UI features as research or diagnostic options rather than the default production foundation. The corresponding feature references are linked in the opening table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites for a new UE5.8 project
&lt;/h2&gt;

&lt;p&gt;Adding modules to &lt;code&gt;Build.cs&lt;/code&gt; does not enable plugins. Enable the plugins through the Plugins window or the project's &lt;code&gt;.uproject&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;The following is a &lt;strong&gt;&lt;code&gt;Plugins&lt;/code&gt; fragment to merge into an existing &lt;code&gt;.uproject&lt;/code&gt; file&lt;/strong&gt;, not a complete project file. Enable only the features the project actually uses: plain UMG does not require CommonUI or MVVM.&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="nl"&gt;"Plugins"&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="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"CommonUI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&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="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"EnhancedInput"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&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="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ModelViewViewModel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&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;h3&gt;
  
  
  CommonUI setup order
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Enable &lt;code&gt;CommonUI&lt;/code&gt; and, when required, &lt;code&gt;EnhancedInput&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Game Viewport Client Class&lt;/strong&gt; to &lt;code&gt;CommonGameViewportClient&lt;/code&gt; or a subclass.&lt;/li&gt;
&lt;li&gt;Assign Input Data and platform-specific Controller Data in &lt;strong&gt;Common Input Settings&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;For the current integration path, enable &lt;strong&gt;Enhanced Input Support&lt;/strong&gt;, configure the Input Action metadata, and register the actions in an IMC.&lt;/li&gt;
&lt;li&gt;Reference the current Accept/Back Input Actions from &lt;code&gt;CommonUIInputData&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When retaining legacy configuration, avoid registering the same action through both the DataTable and current paths.&lt;/li&gt;
&lt;li&gt;Test keyboard/mouse, gamepad, device switching, and the complete pause → modal → gameplay sequence on the target hardware.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The viewport client forwards input into the CommonUI router and lets unhandled input continue toward gameplay. Keeping an unconditional duplicate action handler in the PlayerController can cause the same action to fire twice. See the &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/commonui-input-technical-guide-for-unreal-engine" rel="noopener noreferrer"&gt;CommonUI Input Technical Guide&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  MVVM creation types
&lt;/h3&gt;

&lt;p&gt;Enable the UMG Viewmodel plugin, then choose how the Viewmodel is supplied in the Widget Designer's &lt;strong&gt;Viewmodels&lt;/strong&gt; panel. See &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/umg-viewmodel-for-unreal-engine" rel="noopener noreferrer"&gt;UMG Viewmodel&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Creation type&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;Create Instance&lt;/td&gt;
&lt;td&gt;Automatically create a Viewmodel for each widget.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Inject an existing instance created by the owner or Presenter.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global Viewmodel Collection&lt;/td&gt;
&lt;td&gt;Retrieve an instance from the MVVM Subsystem's shared collection.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Property Path&lt;/td&gt;
&lt;td&gt;Resolve an instance through a property/function path.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolver&lt;/td&gt;
&lt;td&gt;Delegate acquisition or creation to a resolver object.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;Obtain the Viewmodel from a local context provider. A parent widget binding or external Context Extension can supply it; manual assignment is also possible.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The UE5.8 enum includes &lt;strong&gt;Resolver&lt;/strong&gt; and &lt;strong&gt;Context&lt;/strong&gt;, not just the four older options. See the &lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Plugins/ModelViewViewModelBlueprint/EMVVMBlueprintVi-" rel="noopener noreferrer"&gt;Creation Type API&lt;/a&gt; and &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-5-8-release-notes" rel="noopener noreferrer"&gt;UE5.8 release notes&lt;/a&gt;. Shared context resolution is defined through &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/ModelViewViewModel/UMVVMViewModelContextResolver" rel="noopener noreferrer"&gt;&lt;code&gt;UMVVMViewModelContextResolver&lt;/code&gt;&lt;/a&gt;. These remain part of the Beta MVVM feature and need regression coverage.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Build.cs&lt;/code&gt; dependencies
&lt;/h3&gt;

&lt;p&gt;Merge these lines into the existing module rules constructor; they are not a complete &lt;code&gt;Build.cs&lt;/code&gt; file.&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;PublicDependencyModuleNames&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="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;"UMG"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Add only when UMyUIManager.cpp directly uses FSlateApplication / FSlateUser.&lt;/span&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="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Slate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"SlateCore"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add only the modules whose C++ types the project actually uses. CommonUI classes require &lt;code&gt;CommonUI&lt;/code&gt;; Common Input types require &lt;code&gt;CommonInput&lt;/code&gt;; Enhanced Input types require &lt;code&gt;EnhancedInput&lt;/code&gt;; and MVVM APIs require &lt;code&gt;ModelViewViewModel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FKey&lt;/code&gt; and related input types require &lt;code&gt;InputCore&lt;/code&gt;. Direct Slate usage, such as &lt;code&gt;SWidget&lt;/code&gt;, &lt;code&gt;FReply&lt;/code&gt;, or the focus APIs used below, requires the appropriate &lt;code&gt;Slate&lt;/code&gt; / &lt;code&gt;SlateCore&lt;/code&gt; dependencies. Dependencies exposed through public headers belong in &lt;code&gt;PublicDependencyModuleNames&lt;/code&gt;; implementation-only dependencies belong in &lt;code&gt;PrivateDependencyModuleNames&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The split-screen-aware &lt;code&gt;RestoreGameplayInput()&lt;/code&gt; example directly uses &lt;code&gt;FSlateApplication&lt;/code&gt; and &lt;code&gt;FSlateUser&lt;/code&gt; in a &lt;code&gt;.cpp&lt;/code&gt; file, so &lt;strong&gt;that example requires &lt;code&gt;Slate&lt;/code&gt; and &lt;code&gt;SlateCore&lt;/code&gt; as private dependencies&lt;/strong&gt;. They are not blanket prerequisites for every plain UMG project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the same responsibility model in Blueprint and C++
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;th&gt;Blueprint-first project&lt;/th&gt;
&lt;th&gt;C++-first project&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;State ownership&lt;/td&gt;
&lt;td&gt;Actor Component, PlayerState, or Subsystem&lt;/td&gt;
&lt;td&gt;Native class, service, or Gameplay Ability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Presentation&lt;/td&gt;
&lt;td&gt;Blueprint Presenter or Viewmodel; the owner for a simple screen&lt;/td&gt;
&lt;td&gt;UObject Presenter or native Viewmodel; the owner for a simple screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget ownership&lt;/td&gt;
&lt;td&gt;BP_PlayerController, BP_HUD, or a UI manager exposed to Blueprint through C++ when needed&lt;/td&gt;
&lt;td&gt;PlayerController, HUD, or a &lt;code&gt;ULocalPlayerSubsystem&lt;/code&gt; subclass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input routing&lt;/td&gt;
&lt;td&gt;Enhanced Input, CommonUI, and BP_UIManager&lt;/td&gt;
&lt;td&gt;CommonUI Action Router, PlayerController, and UI manager&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  A Blueprint-first ammunition HUD
&lt;/h3&gt;

&lt;p&gt;The same architecture can be implemented without moving every responsibility into native code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expose the source contract.&lt;/strong&gt; &lt;code&gt;BP_AmmoComponent&lt;/code&gt; provides getters, a &lt;code&gt;RequestReload&lt;/code&gt; command, and an &lt;code&gt;OnAmmoChanged&lt;/code&gt; Event Dispatcher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create and retain the widget.&lt;/strong&gt; BP_PlayerController or BP_UIManager creates &lt;code&gt;WBP_AmmoHUD&lt;/code&gt; with an Owning Player, stores it in a variable, and calls &lt;strong&gt;Add to Player Screen&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track Pawn changes.&lt;/strong&gt; The owner binds once to &lt;strong&gt;On Possessed Pawn Changed&lt;/strong&gt;, immediately synchronizes with &lt;strong&gt;Get Controlled Pawn&lt;/strong&gt;, and calls &lt;code&gt;WBP_AmmoHUD.InitializeAmmoHUD(AmmoComponent)&lt;/code&gt; whenever the Pawn changes—including &lt;code&gt;None&lt;/code&gt; and spectator transitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconnect explicitly.&lt;/strong&gt; &lt;code&gt;InitializeAmmoHUD&lt;/code&gt; unbinds the same Custom Event from the previously retained component, stores the new component, binds again, and immediately reads the getters. With &lt;code&gt;None&lt;/code&gt;, reset the display rather than keeping the previous Pawn's ammunition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Close symmetrically.&lt;/strong&gt; &lt;code&gt;CloseAmmoHUD&lt;/code&gt; unbinds that same Custom Event. Do not use &lt;strong&gt;Unbind All&lt;/strong&gt; and accidentally remove other listeners.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send commands instead of setting state.&lt;/strong&gt; The reload button calls the retained component, or uses &lt;code&gt;BPI_AmmoCommands&lt;/code&gt; to reach a Subsystem command. It does not directly overwrite ammunition values.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Choose the binding pair that matches the screen's lifetime: Construct/Destruct for removal and re-addition, Activated/Deactivated for CommonUI interaction, or explicit Open/Close for a screen that only changes visibility. Reopening must reconnect and immediately synchronize the current values.&lt;/p&gt;

&lt;p&gt;A Blueprint Interface defines a command/getter boundary; it does not replace an Event Dispatcher. In a networked game, connect the command to the appropriate Server RPC or Gameplay Ability rather than assuming that a UI call has gameplay authority.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to move a responsibility into C++
&lt;/h3&gt;

&lt;p&gt;Move code when there is a concrete benefit: shared native type contracts, reuse, reviewable text diffs, automated tests, asynchronous or networking boundaries, or a measured processing bottleneck. This is not a claim that Blueprint lacks type checking.&lt;/p&gt;

&lt;p&gt;“Blueprint is slow” is not a sufficient reason by itself. Layout, styles, animations, and hover/focus feedback can remain in Widget Blueprints while state and lifecycle contracts move into C++ incrementally.&lt;/p&gt;

&lt;h2&gt;
  
  
  C++ example: connect creation, ownership, initial state, and cleanup
&lt;/h2&gt;

&lt;p&gt;The ammunition HUD has three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;UAmmoSourceComponent&lt;/code&gt; owns the state-facing contract.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UAmmoWidget&lt;/code&gt; renders the values and emits a reload request.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AMyPlayerController&lt;/code&gt; owns the widget and acts as the thin presentation adapter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Replace &lt;code&gt;MYGAME_API&lt;/code&gt; with your module's API macro. The source component below defines the public contract used by the controller; it is not a complete replicated weapon system.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Define the source component contract
&lt;/h3&gt;

&lt;p&gt;The component declares the native delegate type, a reference-returning delegate getter, value getters, a state-change broadcast, and &lt;code&gt;RequestReload()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Weapon calculations, replication, and server validation are deliberately outside this minimal example. A &lt;code&gt;BlueprintNativeEvent&lt;/code&gt; places its default C++ implementation in &lt;code&gt;_Implementation&lt;/code&gt;; see &lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/ufunctions-in-unreal-engine" rel="noopener noreferrer"&gt;UFunctions&lt;/a&gt;. Here, reloading intentionally does nothing until a Blueprint override or C++ subclass forwards the request to a Server RPC, Ability, or weapon system.&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;// AmmoSourceComponent.h&lt;/span&gt;
&lt;span class="cp"&gt;#pragma once
#include&lt;/span&gt; &lt;span class="cpf"&gt;"CoreMinimal.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Components/ActorComponent.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"AmmoSourceComponent.generated.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="n"&gt;DECLARE_MULTICAST_DELEGATE_TwoParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FOnAmmoChangedNative&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;int32&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="n"&gt;ClassGroup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Combat&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;BlueprintSpawnableComponent&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;MYGAME_API&lt;/span&gt; &lt;span class="n"&gt;UAmmoSourceComponent&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;UActorComponent&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;FOnAmmoChangedNative&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;OnAmmoChanged&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;AmmoChanged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;UFUNCTION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BlueprintPure&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;"Ammo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;int32&lt;/span&gt; &lt;span class="nf"&gt;GetCurrentAmmo&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="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;Clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CurrentAmmo&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;GetMagazineSize&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;UFUNCTION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BlueprintPure&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;"Ammo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;int32&lt;/span&gt; &lt;span class="nf"&gt;GetMagazineSize&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="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="n"&gt;MagazineSize&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="n"&gt;UFUNCTION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BlueprintCallable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BlueprintNativeEvent&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;"Ammo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;RequestReload&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="nf"&gt;RequestReload_Implementation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ApplyAmmoStateFromGameplay&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;NewCurrent&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;NewMagazine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt;&lt;span class="o"&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;Transient&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;CurrentAmmo&lt;/span&gt; &lt;span class="o"&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;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;Category&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Ammo"&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;ClampMin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"1"&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;MagazineSize&lt;/span&gt; &lt;span class="o"&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;FOnAmmoChangedNative&lt;/span&gt; &lt;span class="n"&gt;AmmoChanged&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="c1"&gt;// AmmoSourceComponent.cpp&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"AmmoSourceComponent.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;UAmmoSourceComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RequestReload_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;// In production, forward to a Server RPC, Ability, or weapon system.&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;UAmmoSourceComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ApplyAmmoStateFromGameplay&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;NewCurrent&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;NewMagazine&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;Magazine&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="n"&gt;NewMagazine&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;const&lt;/span&gt; &lt;span class="n"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;Current&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;Clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NewCurrent&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;Magazine&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;CurrentAmmo&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Current&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;MagazineSize&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Magazine&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;CurrentAmmo&lt;/span&gt; &lt;span class="o"&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;MagazineSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Magazine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;AmmoChanged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CurrentAmmo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MagazineSize&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 native delegate is not a &lt;code&gt;UPROPERTY&lt;/code&gt;. A subscriber retains its &lt;code&gt;FDelegateHandle&lt;/code&gt; and removes it from the &lt;strong&gt;same delegate instance&lt;/strong&gt; when disconnecting. Read the getters for the initial display; do not wait for the next change notification.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Define the display widget
&lt;/h3&gt;

&lt;p&gt;In the derived Widget Blueprint, connect the button's &lt;code&gt;OnClicked&lt;/code&gt; event to the widget's &lt;code&gt;RequestReload&lt;/code&gt; function. The widget emits an intent; the controller forwards it to the current source.&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;// AmmoWidget.h&lt;/span&gt;
&lt;span class="cp"&gt;#pragma once
#include&lt;/span&gt; &lt;span class="cpf"&gt;"CoreMinimal.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Blueprint/UserWidget.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Components/TextBlock.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"AmmoWidget.generated.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="n"&gt;DECLARE_DYNAMIC_MULTICAST_DELEGATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FOnAmmoReloadClicked&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="n"&gt;Abstract&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;MYGAME_API&lt;/span&gt; &lt;span class="n"&gt;UAmmoWidget&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;UUserWidget&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;UFUNCTION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BlueprintCallable&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;"HUD"&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;SetAmmo&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;Current&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;Magazine&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;ensure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AmmoText&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;const&lt;/span&gt; &lt;span class="n"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;SafeMagazine&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="n"&gt;Magazine&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;Current&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;Clamp&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SafeMagazine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;AmmoText&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FText&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;NSLOCTEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AmmoWidget"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"AmmoFormat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"{0} / {1}"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;FText&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AsNumber&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;FText&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AsNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SafeMagazine&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;UFUNCTION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BlueprintCallable&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;"HUD"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;RequestReload&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;OnReloadClicked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Broadcast&lt;/span&gt;&lt;span class="p"&gt;();&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;BlueprintAssignable&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;"HUD"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;FOnAmmoReloadClicked&lt;/span&gt; &lt;span class="n"&gt;OnReloadClicked&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt;&lt;span class="o"&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;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;BindWidget&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;UTextBlock&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AmmoText&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;Add the required Text Block named &lt;strong&gt;&lt;code&gt;AmmoText&lt;/code&gt;&lt;/strong&gt; to the derived Widget Blueprint. Its name and type must satisfy the &lt;code&gt;BindWidget&lt;/code&gt; declaration. See &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/UMG/UMWidget_1" rel="noopener noreferrer"&gt;BindWidget metadata&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Own the HUD in the PlayerController and use one Pawn-change path
&lt;/h3&gt;

&lt;p&gt;Do not use separate &lt;code&gt;OnPossess&lt;/code&gt;, &lt;code&gt;AcknowledgePossession&lt;/code&gt;, and &lt;code&gt;OnUnPossess&lt;/code&gt; overrides just to reconnect the HUD. &lt;code&gt;OnPossessedPawnChanged&lt;/code&gt; is notified on both authority and client, and either the old or new Pawn can be &lt;code&gt;nullptr&lt;/code&gt;. See the &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Engine/AController" rel="noopener noreferrer"&gt;AController API&lt;/a&gt; and &lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/FOnPossessedPawnChanged" rel="noopener noreferrer"&gt;FOnPossessedPawnChanged API&lt;/a&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="c1"&gt;// MyPlayerController.h&lt;/span&gt;
&lt;span class="cp"&gt;#pragma once
#include&lt;/span&gt; &lt;span class="cpf"&gt;"CoreMinimal.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"GameFramework/PlayerController.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"MyPlayerController.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;APawn&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;UAmmoSourceComponent&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;UAmmoWidget&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;MYGAME_API&lt;/span&gt; &lt;span class="n"&gt;AMyPlayerController&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;APlayerController&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;BeginPlay&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;EndPlay&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;EEndPlayReason&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;Reason&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;EditDefaultsOnly&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;"UI"&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;UAmmoWidget&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AmmoWidgetClass&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;Transient&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;UAmmoWidget&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AmmoWidget&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;UAmmoSourceComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AmmoSource&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FDelegateHandle&lt;/span&gt; &lt;span class="n"&gt;AmmoChangedHandle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;UFUNCTION&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;HandlePawnChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;APawn&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;OldPawn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;APawn&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;NewPawn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;UFUNCTION&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;HandleReload&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;BindAmmoHudToPawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;APawn&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;NewPawn&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;UnbindAmmoSource&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;HandleAmmoChanged&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;Current&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;Magazine&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="c1"&gt;// MyPlayerController.cpp&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"MyPlayerController.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"AmmoSourceComponent.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"AmmoWidget.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"GameFramework/Pawn.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;AMyPlayerController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BeginPlay&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;BeginPlay&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;IsLocalController&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;ensure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AmmoWidgetClass&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;AmmoWidget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CreateWidget&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UAmmoWidget&lt;/span&gt;&lt;span class="o"&gt;&amp;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;AmmoWidgetClass&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;ensure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AmmoWidget&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;AmmoWidget&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;OnReloadClicked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddUniqueDynamic&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;AMyPlayerController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;HandleReload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;AmmoWidget&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;AddToPlayerScreen&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;OnPossessedPawnChanged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddUniqueDynamic&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;AMyPlayerController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;HandlePawnChanged&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;BindAmmoHudToPawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GetPawn&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;AMyPlayerController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;HandlePawnChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;APawn&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;OldPawn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;APawn&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;NewPawn&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;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;OldPawn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;BindAmmoHudToPawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NewPawn&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;AMyPlayerController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BindAmmoHudToPawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;APawn&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;NewPawn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;UnbindAmmoSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;UAmmoSourceComponent&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NewPawn&lt;/span&gt;
        &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;NewPawn&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;FindComponentByClass&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UAmmoSourceComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;nullptr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;AmmoSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Source&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;Source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;HandleAmmoChanged&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;1&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;AmmoChangedHandle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Source&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;OnAmmoChanged&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;AddUObject&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;AMyPlayerController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;HandleAmmoChanged&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;HandleAmmoChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Source&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetCurrentAmmo&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;Source&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetMagazineSize&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;AMyPlayerController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;UnbindAmmoSource&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;UAmmoSourceComponent&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AmmoSource&lt;/span&gt;&lt;span class="p"&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;Source&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;AmmoChangedHandle&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="n"&gt;Source&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;OnAmmoChanged&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AmmoChangedHandle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;AmmoChangedHandle&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;AmmoSource&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="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;AMyPlayerController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;HandleAmmoChanged&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;Current&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;Magazine&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;AmmoWidget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;AmmoWidget&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetAmmo&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;Magazine&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;AMyPlayerController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;HandleReload&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;UAmmoSourceComponent&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AmmoSource&lt;/span&gt;&lt;span class="p"&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;Source&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;RequestReload&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;AMyPlayerController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;EndPlay&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;EEndPlayReason&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;Reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;OnPossessedPawnChanged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoveDynamic&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;AMyPlayerController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;HandlePawnChanged&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;UnbindAmmoSource&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;AmmoWidget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;AmmoWidget&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;OnReloadClicked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoveDynamic&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;AMyPlayerController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;HandleReload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;AmmoWidget&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;RemoveFromParent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;AmmoWidget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;nullptr&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;EndPlay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Reason&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;Configure a concrete Widget Blueprint derived from &lt;code&gt;UAmmoWidget&lt;/code&gt; as &lt;code&gt;AmmoWidgetClass&lt;/code&gt;, use the controller class in the game configuration, and give the relevant Pawn a &lt;code&gt;UAmmoSourceComponent&lt;/code&gt; or subclass. Gameplay must feed state changes into the source; the example does not add replication for you.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;BeginPlay&lt;/code&gt; binds once to the Pawn-change notification and immediately synchronizes using &lt;code&gt;GetPawn()&lt;/code&gt;. Every reconnection first unsubscribes from the old source. A null Pawn—or a Pawn without the source component—resets the HUD to &lt;strong&gt;&lt;code&gt;0 / 1&lt;/code&gt;&lt;/strong&gt;, the example's explicit no-source display.&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;Transition&lt;/th&gt;
&lt;th&gt;Acceptance condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Listen server's local player&lt;/td&gt;
&lt;td&gt;Pawn A → &lt;code&gt;nullptr&lt;/code&gt; / spectator → Pawn B&lt;/td&gt;
&lt;td&gt;Pawn A notifications stop immediately; the no-source state resets the HUD; each Pawn B event produces exactly one update.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote owning client&lt;/td&gt;
&lt;td&gt;Pawn A → &lt;code&gt;nullptr&lt;/code&gt; / spectator → Pawn B&lt;/td&gt;
&lt;td&gt;No old subscription or stale Pawn A display remains on the client; each Pawn B event produces exactly one update.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The controller owns this HUD until &lt;code&gt;EndPlay&lt;/code&gt;. A design that temporarily closes the HUD needs its own matching disconnect/reconnect contract; the lifecycle choices in the next section explain how to define one.&lt;/p&gt;

&lt;p&gt;Extract a dedicated class such as &lt;code&gt;UAmmoHudPresenter&lt;/code&gt; when the adapter must combine multiple sources, share formatted text, handle asynchronous loading or replays, or support tests without a live gameplay source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delegate subscriptions need four distinct lifetime contracts
&lt;/h2&gt;

&lt;p&gt;Binding in &lt;code&gt;OnInitialized&lt;/code&gt; and unbinding in &lt;code&gt;Destruct&lt;/code&gt; is an unsafe pairing for a reusable widget instance. After removal and re-addition, &lt;code&gt;OnInitialized&lt;/code&gt; does not run again to restore that subscription.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OnInitialized&lt;/code&gt; normally runs once per instance; &lt;code&gt;Construct&lt;/code&gt; and &lt;code&gt;Destruct&lt;/code&gt; can run multiple times as the underlying widget hierarchy is constructed and destroyed. See &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/UMG/UUserWidget/OnInitialized" rel="noopener noreferrer"&gt;&lt;code&gt;OnInitialized&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/UMG/UUserWidget/Construct" rel="noopener noreferrer"&gt;&lt;code&gt;Construct&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/UMG/UUserWidget/Destruct" rel="noopener noreferrer"&gt;&lt;code&gt;Destruct&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lifetime contract&lt;/th&gt;
&lt;th&gt;Bind / unbind pair&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;Entire widget instance&lt;/td&gt;
&lt;td&gt;Bind once in &lt;code&gt;NativeOnInitialized&lt;/code&gt;; normally do not remove it in &lt;code&gt;NativeDestruct&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;A callback from a child button within the same widget.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Presence in the widget hierarchy&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NativeConstruct&lt;/code&gt; / &lt;code&gt;NativeDestruct&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;External gameplay delegates, timers, or message-bus subscriptions tied to that presence.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active CommonUI interaction&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NativeOnActivated&lt;/code&gt; / &lt;code&gt;NativeOnDeactivated&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Subscriptions and input needed only while the screen is active.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit visibility-based open period&lt;/td&gt;
&lt;td&gt;Explicit &lt;code&gt;Open()&lt;/code&gt; / &lt;code&gt;Close()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A panel that becomes Hidden or Collapsed without being removed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;NativePreConstruct&lt;/code&gt; also runs for Designer previews, so do not access world state there unconditionally.&lt;/p&gt;

&lt;p&gt;Test all four transitions: first display, remove → re-add, deactivate → reactivate, and hide → show. In each case, reconnecting should synchronize immediately, necessary notifications should not disappear, and each event should be delivered exactly once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Restore input after closing the last CommonUI screen
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FUIInputConfig&lt;/code&gt; controls input mode, mouse capture/lock, &lt;code&gt;bHideCursorDuringViewportCapture&lt;/code&gt;, and whether move/look input is ignored. That cursor flag means &lt;strong&gt;hide the cursor while the viewport has capture&lt;/strong&gt;. General visibility policy through &lt;code&gt;APlayerController::bShowMouseCursor&lt;/code&gt; is a separate responsibility. See the &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/CommonUI/FUIInputConfig" rel="noopener noreferrer"&gt;FUIInputConfig API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When no desired configuration is supplied, CommonUI can fall back to the last valid input configuration. Deactivating every Activatable Widget does not necessarily restore gameplay input. See &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/input-fundamentals-for-commonui-in-unreal-engine" rel="noopener noreferrer"&gt;Input Fundamentals for CommonUI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Use one of two explicit policies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep a persistent gameplay Activatable Widget at the root. It returns &lt;code&gt;ECommonInputMode::Game&lt;/code&gt;, the intended mouse-capture policy, and enabled move/look input.&lt;/li&gt;
&lt;li&gt;When removing all UI, let the LocalPlayer UI manager restore gameplay input configuration, &lt;code&gt;bShowMouseCursor&lt;/code&gt;, and viewport focus after the final pop completes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The following is a &lt;strong&gt;method excerpt&lt;/strong&gt; for a &lt;code&gt;UGameplayRoot&lt;/code&gt; class derived from &lt;code&gt;UCommonActivatableWidget&lt;/code&gt;; declare the matching override in that class.&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;TOptional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FUIInputConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;UGameplayRoot&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GetDesiredInputConfig&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;FUIInputConfig&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ECommonInputMode&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Game&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;EMouseCaptureMode&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CapturePermanently_IncludingInitialMouseDown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Hide the cursor while the viewport has capture.&lt;/span&gt;
    &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bIgnoreMoveInput&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;C&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bIgnoreLookInput&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;return&lt;/span&gt; &lt;span class="n"&gt;C&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 excerpt belongs to a &lt;code&gt;UMyUIManager&lt;/code&gt; derived from &lt;code&gt;ULocalPlayerSubsystem&lt;/code&gt;, with &lt;code&gt;RestoreGameplayInput()&lt;/code&gt; declared in its class. It restores viewport focus &lt;strong&gt;only for the Slate User associated with that LocalPlayer&lt;/strong&gt;. Include the CommonUI module and the private Slate dependencies described earlier.&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;"Engine/LocalPlayer.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Framework/Application/SlateApplication.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Framework/Application/SlateUser.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"GameFramework/PlayerController.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Input/CommonUIActionRouterBase.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Input/UIActionBindingHandle.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;UMyUIManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RestoreGameplayInput&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ULocalPlayer&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;LocalPlayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GetLocalPlayer&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;LocalPlayer&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;UCommonUIActionRouterBase&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;LocalPlayer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetSubsystem&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UCommonUIActionRouterBase&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;Router&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;FUIInputConfig&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ECommonInputMode&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Game&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;EMouseCaptureMode&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CapturePermanently_IncludingInitialMouseDown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bIgnoreMoveInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bIgnoreLookInput&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;Router&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetActiveUIInputConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;APlayerController&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;PC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LocalPlayer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetPlayerController&lt;/span&gt;&lt;span class="p"&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;PC&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bShowMouseCursor&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;span class="k"&gt;if&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;FSlateUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SlateUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LocalPlayer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetSlateUser&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FSlateApplication&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;SetUserFocusToGameViewport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;SlateUser&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetUserIndex&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;EFocusCause&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SetDirectly&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;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Engine/ULocalPlayer/GetSlateUser" rel="noopener noreferrer"&gt;&lt;code&gt;ULocalPlayer::GetSlateUser()&lt;/code&gt;&lt;/a&gt; returns the Slate User associated with that LocalPlayer. &lt;code&gt;FSlateApplication&lt;/code&gt; exposes separate APIs for all users, &lt;code&gt;SetAllUserFocusToGameViewport&lt;/code&gt;, and for one specified user, &lt;code&gt;SetUserFocusToGameViewport&lt;/code&gt;. See the &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Slate/FSlateApplication" rel="noopener noreferrer"&gt;FSlateApplication API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not replace the user-specific call with &lt;code&gt;UWidgetBlueprintLibrary::SetFocusToGameViewport()&lt;/code&gt; in a split-screen UI manager.&lt;/strong&gt; That Blueprint utility has no LocalPlayer parameter; the UE5.8 source behavior identified during review delegates to &lt;code&gt;SetAllUserFocusToGameViewport()&lt;/code&gt;. Restrict it to projects that guarantee exactly one LocalPlayer. See the &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/UMG/UWidgetBlueprintLibrary" rel="noopener noreferrer"&gt;UWidgetBlueprintLibrary API&lt;/a&gt;. In a Blueprint-first split-screen project, expose the user-specific operation through a &lt;code&gt;BlueprintCallable&lt;/code&gt; C++ wrapper.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UCommonUIActionRouterBase&lt;/code&gt; itself derives from &lt;code&gt;ULocalPlayerSubsystem&lt;/code&gt; and provides &lt;code&gt;SetActiveUIInputConfig&lt;/code&gt;; see its &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/CommonUI/UCommonUIActionRouterBase" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;. Use &lt;code&gt;RequestRefreshFocus&lt;/code&gt; only with the leaf-most active node, as required by its &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/CommonUI/UCommonActivatableWidget/RequestRefreshFocus" rel="noopener noreferrer"&gt;API contract&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Keyboard / mouse&lt;/th&gt;
&lt;th&gt;Gamepad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Open pause&lt;/td&gt;
&lt;td&gt;Move/look stop; cursor and capture match the menu policy.&lt;/td&gt;
&lt;td&gt;The initial button receives focus and is usable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open a modal&lt;/td&gt;
&lt;td&gt;Only the modal can be operated.&lt;/td&gt;
&lt;td&gt;Back closes exactly one layer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Close the modal&lt;/td&gt;
&lt;td&gt;Control returns to the pause menu.&lt;/td&gt;
&lt;td&gt;Focus returns inside the pause menu.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Close pause&lt;/td&gt;
&lt;td&gt;Viewport focus, capture, and move/look are restored.&lt;/td&gt;
&lt;td&gt;No stale UI focus consumes gameplay input; each input reaches its intended gameplay target once.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The split-screen acceptance test is stronger than “input works for both players.” Close Player 0's pause screen while Player 1 keeps a menu open: only Player 0 returns to the viewport, and Player 1 retains the menu and its focus. Repeat with the players reversed. Both directions must preserve one delivery per input to the correct target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual events, MVVM, and performance
&lt;/h2&gt;

&lt;p&gt;When there are few screens and ownership is clear, manual delegates or Event Dispatchers are enough. Consider a Presenter or MVVM when many widgets share the same state, display transformations need reuse, or Designer bindings and two-way binding provide a concrete benefit.&lt;/p&gt;

&lt;p&gt;The following is a &lt;strong&gt;Viewmodel method excerpt&lt;/strong&gt;, not a complete Viewmodel class. It assumes a &lt;code&gt;CurrentHealth&lt;/code&gt; field and &lt;code&gt;GetHealthText&lt;/code&gt; function configured for FieldNotify, as described in the &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/umg-viewmodel-for-unreal-engine" rel="noopener noreferrer"&gt;UMG Viewmodel documentation&lt;/a&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="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;UPlayerStatusViewModel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SetCurrentHealth&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;NewValue&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;UE_MVVM_SET_PROPERTY_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CurrentHealth&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;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NewValue&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;UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GetHealthText&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 Viewmodel mediates display state and commands. It should not become the owner of damage calculations, persistent inventory storage, or server validation.&lt;/p&gt;

&lt;p&gt;For UI performance, update frequency is often a more useful first question than whether a function was written in Blueprint or C++. Avoid per-frame casts, raw property bindings, repeated &lt;code&gt;SetText&lt;/code&gt; calls with unchanged values, and unnecessary subscriptions for hidden screens. Prefer change events, List View entry reuse, invalidation, and reduced update frequency where appropriate. Apply a Retainer Box after measurement, not as a universal fix. See &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/optimization-guidelines-for-umg-in-unreal-engine" rel="noopener noreferrer"&gt;Optimization Guidelines for UMG&lt;/a&gt; and &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/invalidation-in-slate-and-umg-for-unreal-engine" rel="noopener noreferrer"&gt;Invalidation in Slate and UMG&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reproducible UI test matrix
&lt;/h2&gt;

&lt;p&gt;Record aspect ratio, resolution, input device, world/VR interaction, and accessibility as separate dimensions. The values below are representative test conditions; replace the minimums with the actual supported minimums for your product and record the target platform for each run.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Representative conditions&lt;/th&gt;
&lt;th&gt;Acceptance condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;16:9&lt;/td&gt;
&lt;td&gt;Minimum 1280×720; also 1920×1080 and 3840×2160&lt;/td&gt;
&lt;td&gt;No clipping, overlap, or inaccessible controls.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16:10 / 21:9 / 32:9&lt;/td&gt;
&lt;td&gt;1280×800, 3440×1440, and 5120×1440 when supported&lt;/td&gt;
&lt;td&gt;Anchors, Safe Zones, and maximum widths remain within the design specification.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DPI and language&lt;/td&gt;
&lt;td&gt;Minimum/maximum scaling, English/Japanese, longest translation, maximum digit count&lt;/td&gt;
&lt;td&gt;No clipping, overlap, focus loss, or unstable layout.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyboard / mouse&lt;/td&gt;
&lt;td&gt;Click, Tab, Escape, and pause → modal → gameplay&lt;/td&gt;
&lt;td&gt;Focus, capture, cursor visibility, and gameplay input are correct; no double firing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gamepad&lt;/td&gt;
&lt;td&gt;D-pad/stick, Accept, Back, and device switching&lt;/td&gt;
&lt;td&gt;Initial/restored focus is correct; Back closes one layer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Touch&lt;/td&gt;
&lt;td&gt;Tap, scroll, software keyboard, and safe areas&lt;/td&gt;
&lt;td&gt;Touch targets remain usable; scrolling does not conflict with other controls; the keyboard does not obscure required interaction.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Split-screen&lt;/td&gt;
&lt;td&gt;Two LocalPlayers. Player 0 closes pause while Player 1 keeps a menu open; repeat in reverse.&lt;/td&gt;
&lt;td&gt;Only the closing player's focus returns to the viewport. The other player's menu and focus remain unchanged. Each user's input reaches the correct target exactly once.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pawn transitions&lt;/td&gt;
&lt;td&gt;Pawn A → null / spectator → Pawn B&lt;/td&gt;
&lt;td&gt;Unsubscribe from A, reset with no source, and update once per B event.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget lifetime&lt;/td&gt;
&lt;td&gt;First display, remove/re-add, deactivate/reactivate, and hide/show&lt;/td&gt;
&lt;td&gt;No missing or duplicated notifications.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;World UI / VR&lt;/td&gt;
&lt;td&gt;Occlusion, distance, left/right hands, and ray hit/miss&lt;/td&gt;
&lt;td&gt;Unnecessary updates stop; Virtual User / Pointer assignments do not collide.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility&lt;/td&gt;
&lt;td&gt;Keyboard navigation, contrast, and screen readers on supported platforms&lt;/td&gt;
&lt;td&gt;Reading order, labels, and control reachability match the accessibility specification.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Automation Driver primarily targets desktop keyboard/mouse-style interaction. Test gamepads, touch, and motion controllers on hardware or with a separate harness rather than assuming Automation Driver covers them. See &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/automation-driver-in-unreal-engine" rel="noopener noreferrer"&gt;Automation Driver&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Screen Reader support is &lt;strong&gt;Experimental&lt;/strong&gt;. The documented platform scope includes third-party screen readers on Windows and VoiceOver on iOS; do not assume universal platform coverage. See &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/supporting-screen-readers-in-unreal-engine" rel="noopener noreferrer"&gt;Supporting Screen Readers&lt;/a&gt;. Use &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/using-the-slate-widget-reflector-in-unreal-engine" rel="noopener noreferrer"&gt;Widget Reflector&lt;/a&gt; and &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/slate-insights-in-unreal-engine" rel="noopener noreferrer"&gt;Slate Insights&lt;/a&gt; to inspect focus, hit testing, invalidation, and updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final recommendations
&lt;/h2&gt;

&lt;p&gt;Use &lt;strong&gt;UMG for ordinary screens&lt;/strong&gt;, &lt;strong&gt;CommonUI for screen stacks, Back navigation, and focus&lt;/strong&gt;, &lt;strong&gt;a Presenter or Beta MVVM for complex presentation&lt;/strong&gt;, and &lt;strong&gt;Widget Components for world-space UI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Editor Utility Widgets and UMG Viewmodel are &lt;strong&gt;Beta&lt;/strong&gt;; UIFramework and SlateIM are &lt;strong&gt;Experimental&lt;/strong&gt;. The release notes describe Common UI itself as production-ready, but the Enhanced Input how-to retains older warnings. Keep the current and legacy integration paths separate, and validate the configuration used by the project against the linked documentation and target build.&lt;/p&gt;

&lt;p&gt;Whether the project is Blueprint-first or C++-first, make the same four responsibilities explicit: &lt;strong&gt;state ownership, presentation, widget ownership, and input routing&lt;/strong&gt;. Once those boundaries are clear, the Blueprint/C++ split can evolve without rewriting the whole UI architecture.&lt;/p&gt;

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

&lt;p&gt;Version-sensitive and API-specific claims link directly to the relevant Epic documentation throughout the article. This English adaptation retains the &lt;strong&gt;UE5.8&lt;/strong&gt; scope of the reviewed Japanese edition. That edition records &lt;strong&gt;August 31, 2026&lt;/strong&gt; as its technical reference date; the translation is not a new engine build or runtime validation.&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;Unreal Engine 5.8 Release Notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Engine/AController" rel="noopener noreferrer"&gt;AController API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/FOnPossessedPawnChanged" rel="noopener noreferrer"&gt;FOnPossessedPawnChanged API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Engine/ULocalPlayerSubsystem" rel="noopener noreferrer"&gt;ULocalPlayerSubsystem API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Engine/ULocalPlayer/GetSlateUser" rel="noopener noreferrer"&gt;ULocalPlayer::GetSlateUser API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Slate/FSlateApplication" rel="noopener noreferrer"&gt;FSlateApplication API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/UMG/UWidgetBlueprintLibrary" rel="noopener noreferrer"&gt;UWidgetBlueprintLibrary API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/overview-of-advanced-multiplatform-user-interfaces-with-common-ui-for-unreal-engine" rel="noopener noreferrer"&gt;Common UI Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/using-commonui-with-enhnaced-input-in-unreal-engine" rel="noopener noreferrer"&gt;Using CommonUI With Enhanced Input&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/input-fundamentals-for-commonui-in-unreal-engine" rel="noopener noreferrer"&gt;Input Fundamentals for CommonUI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/CommonUI/UCommonActivatableWidget" rel="noopener noreferrer"&gt;UCommonActivatableWidget API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/CommonUI/FUIInputConfig" rel="noopener noreferrer"&gt;FUIInputConfig API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/umg-viewmodel-for-unreal-engine" rel="noopener noreferrer"&gt;UMG Viewmodel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Plugins/ModelViewViewModelBlueprint/EMVVMBlueprintVi-" rel="noopener noreferrer"&gt;Creation Type API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/ModelViewViewModel/UMVVMViewModelContextResolver" rel="noopener noreferrer"&gt;UMVVMViewModelContextResolver API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/optimization-guidelines-for-umg-in-unreal-engine" rel="noopener noreferrer"&gt;Optimization Guidelines for UMG&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/widget-components-in-unreal-engine" rel="noopener noreferrer"&gt;Widget Components&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&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/supporting-screen-readers-in-unreal-engine" rel="noopener noreferrer"&gt;Supporting Screen Readers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unrealengine</category>
      <category>gamedev</category>
      <category>cpp</category>
      <category>programming</category>
    </item>
    <item>
      <title>HDR in Unity URP: Bloom, Tone Mapping, and HDR Display Output</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Fri, 25 Sep 2026 03:36:48 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/hdr-in-unity-urp-bloom-tone-mapping-and-hdr-display-output-122o</link>
      <guid>https://dev.to/gamedevtoollab/hdr-in-unity-urp-bloom-tone-mapping-and-hdr-display-output-122o</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;HDR, Emission, Bloom, Exposure, and Tonemapping are related but different features. Player Settings adds another: HDR Display Output.&lt;/p&gt;

&lt;p&gt;Separate &lt;strong&gt;brightness during rendering&lt;/strong&gt; from &lt;strong&gt;HDR display output&lt;/strong&gt;, using an SDR-monitor experiment, diagnostics, and custom-rendering examples.&lt;/p&gt;

&lt;p&gt;Sections 1–5 cover internal HDR; 6–8 cover output. Skip advanced sections 9–10 for troubleshooting in section 11 as needed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Scope: Unity 6.3 LTS (6000.3), URP 17.3, English UI.&lt;/strong&gt; Documentation review date recorded in the Japanese source: &lt;strong&gt;September 15, 2026&lt;/strong&gt;. This translation does not revalidate every reference.&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Untested examples:&lt;/strong&gt; Unity compilation, scene execution, Player builds, HDR-display testing, and benchmarking have not been performed for this article. Observation prompts are not measured results.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Two meanings of HDR
&lt;/h2&gt;

&lt;p&gt;HDR means &lt;strong&gt;High Dynamic Range&lt;/strong&gt;. In Unity, distinguish these two uses:&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;HDR rendering&lt;/th&gt;
&lt;th&gt;HDR display output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;Preserve brightness during rendering&lt;/td&gt;
&lt;td&gt;Use an HDR display path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controls&lt;/td&gt;
&lt;td&gt;Camera, shader, buffer, post-processing&lt;/td&gt;
&lt;td&gt;Player, OS, GPU, display&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDR monitor?&lt;/td&gt;
&lt;td&gt;Not for internal processing&lt;/td&gt;
&lt;td&gt;Yes, with compatible output&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;HDR rendering can end in SDR output.&lt;/strong&gt; Games for SDR monitors can preserve HDR internally and map it to the display range afterward.&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High Definition Render Pipeline (HDRP)&lt;/strong&gt; is not required for HDR. URP supports it, and HDRP also needs output configuration.&lt;sup id="fnref3"&gt;3&lt;/sup&gt;&lt;sup id="fnref4"&gt;4&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why values above 1 matter
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A white wall is not an emissive sign
&lt;/h3&gt;

&lt;p&gt;These grayscale shader outputs illustrate scene values, not measured luminance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;White surface:     ( 1,  1,  1)
Emissive surface:  ( 4,  4,  4)
Strong emission:   (16, 16, 16)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clamping them to 0–1 makes all three &lt;code&gt;(1, 1, 1)&lt;/code&gt;. Later processing cannot recover the difference.&lt;/p&gt;

&lt;p&gt;An HDR buffer preserves differences for Bloom and later output conversion. &lt;strong&gt;HDR retains information; it does not simply brighten the image.&lt;/strong&gt;&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tone mapping is not clipping
&lt;/h3&gt;

&lt;p&gt;Tone mapping remaps brightness to the output range. 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;y = x / (1 + x)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input x&lt;/th&gt;
&lt;th&gt;Clamped to 0–1&lt;/th&gt;
&lt;th&gt;Result of the formula&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.941&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This preserves differences within 0–1. &lt;strong&gt;It is not Unity's Neutral or ACES formula.&lt;/strong&gt; Choose URP's operator in the Tonemapping Volume Override.&lt;sup id="fnref5"&gt;5&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Linear, HDR, gamut, and bit depth
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;What it describes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Linear / Gamma&lt;/td&gt;
&lt;td&gt;How lighting and color values relate numerically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDR / SDR&lt;/td&gt;
&lt;td&gt;Brightness range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Color gamut&lt;/td&gt;
&lt;td&gt;Represented/encoded color range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bit depth / format&lt;/td&gt;
&lt;td&gt;Precision and numerical range&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Linear does not enable HDR output. 10-bit storage need not be HDR, and HDR does not make assets wide-gamut.&lt;sup id="fnref6"&gt;6&lt;/sup&gt;&lt;sup id="fnref7"&gt;7&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Set &lt;code&gt;Project Settings &amp;gt; Player &amp;gt; Other Settings &amp;gt; Rendering &amp;gt; Color Space&lt;/code&gt; to &lt;strong&gt;Linear&lt;/strong&gt;. This changes lighting and blending in existing Gamma projects; compare and readjust their appearance.&lt;sup id="fnref8"&gt;8&lt;/sup&gt;&lt;sup id="fnref9"&gt;9&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not disable &lt;code&gt;sRGB (Color Texture)&lt;/code&gt; everywhere.&lt;/strong&gt; Color textures and data masks differ. Disabling it skips sRGB-to-Linear sampling conversion; choose according to the data.&lt;sup id="fnref10"&gt;10&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Compare HDR on an SDR monitor
&lt;/h2&gt;

&lt;p&gt;Disable HDR display output initially to isolate internal HDR from OS/display settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Check the active URP Asset
&lt;/h3&gt;

&lt;p&gt;An asset assigned to the current Quality level overrides the default. Identify the active asset with &lt;code&gt;GraphicsSettings.currentRenderPipeline&lt;/code&gt;.&lt;sup id="fnref11"&gt;11&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Enable its &lt;strong&gt;HDR&lt;/strong&gt; setting and set &lt;code&gt;Post-processing &amp;gt; Grading Mode&lt;/code&gt; to &lt;strong&gt;High Dynamic Range&lt;/strong&gt;. Grading Mode controls grading, not display output.&lt;sup id="fnref12"&gt;12&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Configure the camera and Volume
&lt;/h3&gt;

&lt;p&gt;Use one Base Camera without a target RenderTexture. Enable &lt;code&gt;Output &amp;gt; HDR Rendering&lt;/code&gt; and &lt;code&gt;Rendering &amp;gt; Post Processing&lt;/code&gt; in its Inspector.&lt;sup id="fnref13"&gt;13&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;GameObject &amp;gt; Volume &amp;gt; Global Volume&lt;/code&gt; with a new Profile and Weight 1. Include its Layer in the camera's Volume Mask. Disable other Volumes and custom Renderer Features.&lt;sup id="fnref14"&gt;14&lt;/sup&gt;&lt;sup id="fnref13"&gt;13&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Add Bloom and Tonemapping. Enable each edited setting's override checkbox:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Override&lt;/th&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Comparison value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bloom&lt;/td&gt;
&lt;td&gt;Threshold&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bloom&lt;/td&gt;
&lt;td&gt;Intensity&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bloom&lt;/td&gt;
&lt;td&gt;Scatter&lt;/td&gt;
&lt;td&gt;0.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tonemapping&lt;/td&gt;
&lt;td&gt;Mode&lt;/td&gt;
&lt;td&gt;None initially&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are examples, not defaults or universal recommendations. Leave other color/exposure adjustments off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bloom Threshold uses gamma-space brightness.&lt;/strong&gt; Threshold 2 does not directly select Linear shader values above 2.&lt;sup id="fnref15"&gt;15&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;URP includes Volume-based post-processing; do not mix in Post Processing Stack v2.&lt;sup id="fnref16"&gt;16&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Build an HDR Unlit Shader Graph
&lt;/h3&gt;

&lt;p&gt;Create a &lt;strong&gt;Universal Unlit Shader Graph&lt;/strong&gt; to avoid lighting effects. Expose a Color property named &lt;code&gt;HDR Color&lt;/code&gt;, Mode &lt;strong&gt;HDR&lt;/strong&gt;, connected to Fragment &lt;strong&gt;Base Color&lt;/strong&gt;. Keep Surface Type &lt;strong&gt;Opaque&lt;/strong&gt;.&lt;sup id="fnref17"&gt;17&lt;/sup&gt;&lt;sup id="fnref18"&gt;18&lt;/sup&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HDR Color property
    Mode: HDR
        |
        +-- Fragment / Base Color
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create three materials, with Alpha 1. Enter RGB using the HDR picker's float display:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Material&lt;/th&gt;
&lt;th&gt;RGB&lt;/th&gt;
&lt;th&gt;Intensity from white&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;(1, 1, 1)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;(4, 4, 4)&lt;/td&gt;
&lt;td&gt;+2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;(16, 16, 16)&lt;/td&gt;
&lt;td&gt;+4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each Intensity step doubles RGB: +2 means ×4, +4 means ×16—not ×4. Reopening the picker can change its color/intensity decomposition without changing stored RGB.&lt;sup id="fnref19"&gt;19&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Assign A, B, C to Cubes at &lt;code&gt;(-1.5, 0, 0)&lt;/code&gt;, &lt;code&gt;(0, 0, 0)&lt;/code&gt;, &lt;code&gt;(1.5, 0, 0)&lt;/code&gt;. Use camera position &lt;code&gt;(0, 0, -6)&lt;/code&gt;, rotation &lt;code&gt;(0, 0, 0)&lt;/code&gt;, Orthographic Size 2, a dark solid background, and 16:9 Game View.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 Change one variable at a time
&lt;/h3&gt;

&lt;p&gt;These are observation prompts, not results recorded in Unity:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration (Bloom values are Intensity)&lt;/th&gt;
&lt;th&gt;What to examine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HDR on, Bloom 0, Tonemapping None&lt;/td&gt;
&lt;td&gt;Does SDR hide differences in the white surfaces?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDR on, Bloom 0.5, Tonemapping None&lt;/td&gt;
&lt;td&gt;Do similar whites produce different glow?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDR on, Bloom on, Neutral&lt;/td&gt;
&lt;td&gt;How do highlights and surroundings change?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same setup, HDR Rendering off&lt;/td&gt;
&lt;td&gt;How does losing HDR differences affect Bloom?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Look for &lt;strong&gt;different values behind apparently identical whites&lt;/strong&gt;, not just glow.&lt;/p&gt;

&lt;p&gt;Bloom also works on 0–1 images with a low Threshold. HDR distinguishes brighter-than-white values.&lt;sup id="fnref15"&gt;15&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Separate Emission, Bloom, exposure, and tone mapping
&lt;/h2&gt;

&lt;p&gt;This conceptual flow is not an exact render-pass schedule:&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;sup id="fnref20"&gt;20&lt;/sup&gt;&lt;sup id="fnref21"&gt;21&lt;/sup&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shaders and lighting
    -&amp;gt; HDR scene color
    -&amp;gt; Image effects such as Bloom
    -&amp;gt; Post Exposure and color adjustments
    -&amp;gt; Tone mapping
    -&amp;gt; SDR or HDR display output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Emission is not Bloom
&lt;/h3&gt;

&lt;p&gt;Emission represents a Lit material's self-emission; Bloom spreads bright image regions. Increasing Emission does not enable Bloom.&lt;sup id="fnref22"&gt;22&lt;/sup&gt;&lt;sup id="fnref15"&gt;15&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Emission does not guarantee nearby illumination; check the GI setup and method. An additional Light can illuminate an emissive sign's surroundings.&lt;sup id="fnref22"&gt;22&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;This class-field declaration enables an HDR color picker, not camera HDR:&lt;sup id="fnref23"&gt;23&lt;/sup&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;SerializeField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;ColorUsage&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;true&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;Color&lt;/span&gt; &lt;span class="n"&gt;emissionColor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Post Exposure comes after HDR effects
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Post Exposure&lt;/strong&gt; in Color Adjustments runs after HDR effects, before tone mapping. Raising it is not equivalent to raising Bloom's input.&lt;sup id="fnref20"&gt;20&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Choose the emitter, Bloom extraction, or processed-image exposure control according to your goal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compare Neutral and ACES by appearance
&lt;/h3&gt;

&lt;p&gt;Neutral minimizes hue/saturation changes. ACES approximates the reference ACES tone mapper, with stronger contrast and hue/saturation changes.&lt;sup id="fnref5"&gt;5&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;For stylized emission, compare Neutral with ACES. Choose the intended look; ACES is not automatically better.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Enable HDR display output
&lt;/h2&gt;

&lt;p&gt;URP HDR output requires &lt;strong&gt;Linear&lt;/strong&gt;, an HDR-capable environment, and HDR in the active URP Asset. Check Player &lt;code&gt;Other Settings&lt;/code&gt;:&lt;sup id="fnref21"&gt;21&lt;/sup&gt;&lt;sup id="fnref3"&gt;3&lt;/sup&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Allow HDR Display Output&lt;/td&gt;
&lt;td&gt;Permit HDR output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use HDR Display Output&lt;/td&gt;
&lt;td&gt;Use HDR on the main display&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A monitor alone is insufficient: check OS HDR settings, GPU, driver, and Graphics API.&lt;sup id="fnref7"&gt;7&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Start Windows testing with a D3D12 Player. Unity 6.3 URP also lists D3D11 and Vulkan. &lt;strong&gt;D3D11 HDR is Player-only, not available in the Editor or exclusive fullscreen. Use borderless fullscreen.&lt;/strong&gt;&lt;sup id="fnref21"&gt;21&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;HDR output still maps to the display range rather than sending scene values unchanged.&lt;sup id="fnref21"&gt;21&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Inspect HDR output in code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  7.1 Read output and request status
&lt;/h3&gt;

&lt;p&gt;Read &lt;code&gt;HDROutputSettings.main&lt;/code&gt;: &lt;code&gt;available&lt;/code&gt; includes the main display and project settings; &lt;code&gt;active&lt;/code&gt; reports current HDR output. Availability is not a monitor-hardware-only test.&lt;sup id="fnref24"&gt;24&lt;/sup&gt;&lt;sup id="fnref25"&gt;25&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HDRModeChangeRequested&lt;/code&gt; reports a pending request, not current output. This diagnostic blocks further requests while one is pending; the caller checks completion later.&lt;sup id="fnref26"&gt;26&lt;/sup&gt;&lt;sup id="fnref27"&gt;27&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Save &lt;code&gt;HdrOutputProbe.cs&lt;/code&gt; and attach it to a GameObject. It uses UnityEngine only. The source revision checked Unity 6.3 API/C# declarations, not compilation or execution. &lt;strong&gt;&lt;code&gt;HDRModeChangeRequested&lt;/code&gt; is a property, without &lt;code&gt;()&lt;/code&gt;.&lt;/strong&gt;&lt;sup id="fnref28"&gt;28&lt;/sup&gt;&lt;sup id="fnref29"&gt;29&lt;/sup&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DisallowMultipleComponent&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;HdrOutputProbe&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;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="nf"&gt;LogStatus&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;ContextMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HDR/Log Status"&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;LogStatus&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;hdr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HDROutputSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&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;hdr&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Could not retrieve HDR output settings."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;available&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;active&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&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;$"ColorSpace=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;QualitySettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activeColorSpace&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;$"HDR available=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, active=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Do not read request status or luminance data when HDR is unavailable.&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;available&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;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Skipped HDR details because HDR is unavailable."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;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;$"API automaticHDRTonemapping=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;automaticHDRTonemapping&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;$"HDRModeChangeRequested=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HDRModeChangeRequested&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;active&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;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HDR output is inactive. Skipped luminance data."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// These are API values, not the effective settings of the URP Volume.&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;$"API paperWhiteNits=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paperWhiteNits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;F1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; nits, "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
            &lt;span class="s"&gt;$"minToneMapLuminance=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minToneMapLuminance&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; nits, "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
            &lt;span class="s"&gt;$"maxToneMapLuminance(10%)=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxToneMapLuminance&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; nits, "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
            &lt;span class="s"&gt;$"maxFullFrameToneMapLuminance(100%)="&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;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxFullFrameToneMapLuminance&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; nits"&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="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ContextMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HDR/Request On"&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;RequestOn&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;RequestMode&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="nf"&gt;ContextMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HDR/Request Off"&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;RequestOff&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;RequestMode&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;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;RequestMode&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;enable&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;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isPlaying&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="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;"Call this method while the application is playing."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hdr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HDROutputSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;// Check availability for both enabling and disabling HDR.&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;hdr&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;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;available&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;"Cannot request an HDR output mode change in this environment."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Do not send another request, including a reversal, while one is pending.&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;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HDRModeChangeRequested&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;"An HDR output mode change request is still pending."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;enable&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;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"HDR output is already in the requested state: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="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;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RequestHDRModeChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enable&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;$"Requested HDR output change: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;enable&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;"Call LogStatus later to check the pending state and active output."&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It logs at startup. Use its context menu in Play Mode, or public methods from Player UI Buttons. Install only one &lt;code&gt;HdrOutputProbe.cs&lt;/code&gt;, not both pasted and supplied copies.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.2 Guard both on and off requests
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;RequestHDRModeChange&lt;/code&gt; is documented to throw when HDR is unavailable. &lt;strong&gt;&lt;code&gt;false&lt;/code&gt; is not exempt.&lt;/strong&gt; Both directions use &lt;code&gt;hdr == null || !hdr.available&lt;/code&gt;.&lt;sup id="fnref26"&gt;26&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HDRModeChangeRequested&lt;/code&gt; and &lt;code&gt;paperWhiteNits&lt;/code&gt; also document unavailable-environment exceptions. Read details after checking &lt;code&gt;available&lt;/code&gt;, and luminance only when &lt;code&gt;active&lt;/code&gt;.&lt;sup id="fnref27"&gt;27&lt;/sup&gt;&lt;sup id="fnref30"&gt;30&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Pending-request blocking and same-state skipping are &lt;strong&gt;sample policies&lt;/strong&gt;, not mandatory Unity rules. Checking pending status first avoids treating an unfinished transition as complete.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;available=true&lt;/code&gt; does not guarantee runtime switching everywhere. Returning from a request does not prove completion; call &lt;code&gt;LogStatus()&lt;/code&gt; later to check pending status and &lt;code&gt;active&lt;/code&gt;.&lt;sup id="fnref26"&gt;26&lt;/sup&gt;&lt;sup id="fnref25"&gt;25&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The sample lacks polling, timeouts, and disconnection recovery. Production UI needs availability rechecks, pending-state input restrictions, and completion updates. Save settings after confirming output, not logging a request.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.3 Interpret API values correctly
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;API&lt;/code&gt; identifies HDR output settings, not photometer measurements or effective camera-Volume settings.&lt;sup id="fnref28"&gt;28&lt;/sup&gt;&lt;sup id="fnref31"&gt;31&lt;/sup&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Log field&lt;/th&gt;
&lt;th&gt;Meaning or limitation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;available&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Availability for the main display and project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Current HDR state; compare against the request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HDRModeChangeRequested&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pending change; read only when available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;automaticHDRTonemapping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Common API automatic tone-mapping state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;paperWhiteNits&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reference white for automatic mapping with active HDR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;minToneMapLuminance&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimum input luminance retaining detail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxToneMapLuminance(10%)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maximum input luminance retaining detail: 10% bright area&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxFullFrameToneMapLuminance(100%)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maximum input luminance retaining detail: entire screen bright&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;maxToneMapLuminance&lt;/code&gt; is not an unconditional display peak. &lt;code&gt;(10%)&lt;/code&gt; and &lt;code&gt;(100%)&lt;/code&gt; are log labels, not API names.&lt;sup id="fnref28"&gt;28&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;paperWhiteNits&lt;/code&gt; is documented for automatic HDR tone mapping with active HDR output; reading it does not prove URP uses it. &lt;code&gt;automaticHDRTonemapping=false&lt;/code&gt; does not imply that the URP Tonemapping Volume is disabled.&lt;sup id="fnref30"&gt;30&lt;/sup&gt;&lt;sup id="fnref32"&gt;32&lt;/sup&gt;&lt;sup id="fnref31"&gt;31&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;URP Volumes can override Paper White and luminance limits. Check output status in logs and artistic settings in the camera's Volumes. The probe does not change automatic tone mapping or Paper White.&lt;sup id="fnref31"&gt;31&lt;/sup&gt;&lt;sup id="fnref5"&gt;5&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;available=true, active=false&lt;/code&gt; means available but inactive. &lt;code&gt;available=false&lt;/code&gt; proves neither hardware failure nor an SDR-only monitor. These examples are not captured logs.&lt;sup id="fnref24"&gt;24&lt;/sup&gt;&lt;sup id="fnref25"&gt;25&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Paper White and highlight limits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1 nit = 1 cd/m²&lt;/strong&gt; is a luminance unit, not a relative shader color value.&lt;sup id="fnref28"&gt;28&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paper White&lt;/strong&gt; controls reference white, including UI white—not the display's maximum brightness.&lt;sup id="fnref21"&gt;21&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;For illustration, ordinary white might be 200 nits and highlights reach 1000 nits; UI need not reach the maximum. &lt;strong&gt;These are not universal recommendations.&lt;/strong&gt; Shader &lt;code&gt;1.0&lt;/code&gt; is not inherently 1 or 200 nits; conversion and settings determine that relationship.&lt;sup id="fnref31"&gt;31&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;HDR Tonemapping adds these controls. &lt;strong&gt;Both Neutral and ACES support Paper White.&lt;/strong&gt;&lt;sup id="fnref5"&gt;5&lt;/sup&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Neutral&lt;/th&gt;
&lt;th&gt;ACES&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Automatic white&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Detect Paper White&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Detect Paper White&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual white&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Paper White&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Paper White&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brightness range&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Detect Brightness Limits&lt;/code&gt;, &lt;code&gt;Min Nits&lt;/code&gt;, &lt;code&gt;Max Nits&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ACES Preset&lt;/code&gt;: 1000, 2000, or 4000 nits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Disable &lt;code&gt;Detect Paper White&lt;/code&gt; for manual white in either mode. Neutral exposes &lt;code&gt;Min Nits&lt;/code&gt;/&lt;code&gt;Max Nits&lt;/code&gt; when &lt;code&gt;Detect Brightness Limits&lt;/code&gt; is off; ACES uses a preset. White controls overlap, but range controls differ.&lt;sup id="fnref5"&gt;5&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;An ACES 1000-nit preset does not make UI white 1000 nits. Compare UI and emission, checking Inspector overrides rather than preset names.&lt;sup id="fnref5"&gt;5&lt;/sup&gt;&lt;sup id="fnref21"&gt;21&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Display reports can be inaccurate. Consider user calibration across highlights, menus, subtitles, and loading screens.&lt;sup id="fnref5"&gt;5&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Advanced: preserve HDR data
&lt;/h2&gt;

&lt;h3&gt;
  
  
  9.1 Avoid saturating HDR color
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;saturate&lt;/code&gt; in HLSL and Shader Graph clamps to 0–1: useful for masks, destructive for HDR color before tone mapping.&lt;sup id="fnref33"&gt;33&lt;/sup&gt;&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="c1"&gt;// Expressions inside a shader, not a complete Shader file.&lt;/span&gt;
&lt;span class="kt"&gt;float3&lt;/span&gt; &lt;span class="n"&gt;hdrColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;baseColor&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;intensity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Outputting this as scene color loses differences above 1.&lt;/span&gt;
&lt;span class="kt"&gt;float3&lt;/span&gt; &lt;span class="n"&gt;clippedColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;saturate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hdrColor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treat masks and interpolation weights separately from colors that need an HDR range.&lt;/p&gt;

&lt;h3&gt;
  
  
  9.2 Avoid duplicate color conversions
&lt;/h3&gt;

&lt;p&gt;New Shader Graph HDR Color Nodes use Linear; old graphs may retain compatibility behavior. Unlike color textures, they may not need conversion. Check the input space before adding &lt;code&gt;.linear&lt;/code&gt; or &lt;code&gt;pow&lt;/code&gt;.&lt;sup id="fnref34"&gt;34&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  9.3 Choose an appropriate RenderTexture
&lt;/h3&gt;

&lt;p&gt;Intermediate RenderTexture clipping is irreversible. These formats describe &lt;strong&gt;raw scene-linear color before display encoding&lt;/strong&gt;, not final HDR signals:&lt;sup id="fnref35"&gt;35&lt;/sup&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GraphicsFormat&lt;/th&gt;
&lt;th&gt;Bits/pixel&lt;/th&gt;
&lt;th&gt;Raw values above 1&lt;/th&gt;
&lt;th&gt;Alpha&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;R8G8B8A8_UNorm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;B10G11R11_UFloatPack32&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;Yes, positive values&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;R16G16B16A16_SFloat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;UNorm is not always SDR: HDR10 can use PQ-encoded 10-bit UNorm. &lt;strong&gt;Storage format and signal encoding both matter.&lt;/strong&gt;&lt;sup id="fnref7"&gt;7&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;URP's 64-bit &lt;strong&gt;HDR Precision&lt;/strong&gt; increases precision and bandwidth cost. HDR &lt;strong&gt;Alpha Processing requires 64-bit&lt;/strong&gt;; 32-bit HDR does not necessarily include alpha.&lt;sup id="fnref12"&gt;12&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;A target RenderTexture hides some camera HDR controls and determines target settings. Camera HDR alone does not guarantee a suitable format.&lt;sup id="fnref13"&gt;13&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  9.4 Match the custom-pass stage
&lt;/h3&gt;

&lt;p&gt;Post-processed HDR output may not mean scene-linear color. At or after &lt;code&gt;AfterRenderingPostProcessing&lt;/code&gt;, account for gamut and luminance conversion.&lt;sup id="fnref31"&gt;31&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;For post-processing overlays or custom UI, check white levels and duplicate conversion. A float RenderTexture cannot prevent a doubled output transform.&lt;sup id="fnref36"&gt;36&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  9.5 EXR cannot recover discarded values
&lt;/h3&gt;

&lt;p&gt;Unity can encode EXR, but the source must retain HDR data. EXR cannot restore values already lost to tone mapping or clamping.&lt;sup id="fnref37"&gt;37&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Advanced: memory and performance
&lt;/h2&gt;

&lt;p&gt;HDR need not double color-buffer storage: 32-bit HDR exists. At fixed resolution, 32 to 64 bits doubles an uncompressed buffer's raw size.&lt;sup id="fnref35"&gt;35&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;At 3840 × 2160:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;32 bits/pixel:
3840 × 2160 × 4 bytes / 1024² ≈ 31.64 MiB

64 bits/pixel:
3840 × 2160 × 8 bytes / 1024² ≈ 63.28 MiB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These theoretical sizes cover &lt;strong&gt;one color buffer&lt;/strong&gt;, not depth, MSAA, copies, history, or alignment. They are not measured application memory.&lt;/p&gt;

&lt;p&gt;Compare HDR, precision, resolution, and Bloom separately, including high-quality Bloom filtering's added cost and benefit.&lt;sup id="fnref15"&gt;15&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;On the same hardware and resolution, toggle HDR, 32/64-bit precision, and Bloom individually. Record GPU time and appearance.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Troubleshooting
&lt;/h2&gt;

&lt;p&gt;These are starting points, not diagnoses:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Check first&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No visible HDR difference&lt;/td&gt;
&lt;td&gt;Above-1 values and effects that reveal them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emission without glow&lt;/td&gt;
&lt;td&gt;Post Processing, Volume Mask, Bloom override/Intensity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ordinary whites glow&lt;/td&gt;
&lt;td&gt;Emitter values and Bloom Threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDR display receives SDR&lt;/td&gt;
&lt;td&gt;OS/Player, Linear, Graphics API, &lt;code&gt;available&lt;/code&gt;/&lt;code&gt;active&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI too bright&lt;/td&gt;
&lt;td&gt;Paper White, composition stage, output conversion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDR lost after a custom effect&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;saturate&lt;/code&gt;, buffer formats, repeated tone mapping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intermediate alpha lost&lt;/td&gt;
&lt;td&gt;Target format, HDR Precision, Alpha Processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editor/Player mismatch&lt;/td&gt;
&lt;td&gt;Quality pipeline asset and Graphics API constraints&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use &lt;code&gt;Window &amp;gt; Analysis &amp;gt; Render Pipeline Debugger &amp;gt; Lighting &amp;gt; HDR Debug Mode&lt;/code&gt; to inspect gamut and regions above Paper White, rather than relying only on the final image.&lt;sup id="fnref38"&gt;38&lt;/sup&gt;&lt;/p&gt;

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

&lt;p&gt;Track &lt;strong&gt;where brightness is created, preserved, and converted for display&lt;/strong&gt;. Compare 1, 4, and 16 with internal HDR, Bloom, and tone mapping; then inspect display output and Paper White.&lt;/p&gt;

&lt;p&gt;Separate rendering from display-path problems, ordinary white from highlights, and always check SDR too.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/com.unity.render-pipelines.universal.html" rel="noopener noreferrer"&gt;Universal RP package — Unity 6.3&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/introduction-hdr.html" rel="noopener noreferrer"&gt;Introduction to high dynamic range (HDR)&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/post-processing/enable-hdr-output-urp.html" rel="noopener noreferrer"&gt;Enable HDR Output in URP&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;Unity HDRP 17.3 documentation, &lt;a href="https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@17.3/manual/HDR-Output.html" rel="noopener noreferrer"&gt;HDR Output&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/post-processing-tonemapping.html" rel="noopener noreferrer"&gt;Tonemapping Volume Override reference for URP&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/color-spaces.html" rel="noopener noreferrer"&gt;Color spaces&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;Microsoft Learn, &lt;a href="https://learn.microsoft.com/en-us/windows/win32/direct3darticles/high-dynamic-range" rel="noopener noreferrer"&gt;Use DirectX with Advanced Color on high/standard dynamic range displays&lt;/a&gt;. See the distinctions between storage formats and encoding, and between bit depth, gamut, and dynamic range.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/set-project-color-space.html" rel="noopener noreferrer"&gt;Set the project color space&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn9"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/differences-linear-gamma-color-space.html" rel="noopener noreferrer"&gt;Differences between linear and gamma color spaces&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn10"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/disable-srgb-sampling-textures.html" rel="noopener noreferrer"&gt;Disable sRGB sampling for a texture&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn11"&gt;
&lt;p&gt;Unity Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/QualitySettings-renderPipeline.html" rel="noopener noreferrer"&gt;QualitySettings.renderPipeline&lt;/a&gt;, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Rendering.GraphicsSettings-currentRenderPipeline.html" rel="noopener noreferrer"&gt;GraphicsSettings.currentRenderPipeline&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn12"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/universalrp-asset.html" rel="noopener noreferrer"&gt;URP Asset reference&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn13"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/camera-component-reference.html" rel="noopener noreferrer"&gt;Camera component reference for URP&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn14"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/add-post-processing.html" rel="noopener noreferrer"&gt;Add post-processing in URP&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn15"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/post-processing-bloom.html" rel="noopener noreferrer"&gt;Bloom Volume Override reference for URP&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn16"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/integration-with-post-processing.html" rel="noopener noreferrer"&gt;Post-processing in URP&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn17"&gt;
&lt;p&gt;Unity Shader Graph 17.3 documentation, &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Property-Types.html" rel="noopener noreferrer"&gt;Property Types&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn18"&gt;
&lt;p&gt;Unity Shader Graph 17.3 documentation, &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/First-Shader-Graph.html" rel="noopener noreferrer"&gt;Your first Shader Graph&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn19"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/hdr-color-picker-reference.html" rel="noopener noreferrer"&gt;HDR color picker reference&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn20"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/Post-Processing-Color-Adjustments.html" rel="noopener noreferrer"&gt;Color Adjustments Volume Override reference for URP&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn21"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/post-processing/hdr-output.html" rel="noopener noreferrer"&gt;HDR Output in URP&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn22"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/lit-shader.html" rel="noopener noreferrer"&gt;Lit shader reference for URP&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn23"&gt;
&lt;p&gt;Unity Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/ColorUsageAttribute.html" rel="noopener noreferrer"&gt;ColorUsageAttribute&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn24"&gt;
&lt;p&gt;Unity Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/HDROutputSettings-available.html" rel="noopener noreferrer"&gt;HDROutputSettings.available&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn25"&gt;
&lt;p&gt;Unity Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/HDROutputSettings-active.html" rel="noopener noreferrer"&gt;HDROutputSettings.active&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn26"&gt;
&lt;p&gt;Unity Scripting API, &lt;a href="https://docs.unity3d.com/6000.0/Documentation/ScriptReference/HDROutputSettings.RequestHDRModeChange.html" rel="noopener noreferrer"&gt;HDROutputSettings.RequestHDRModeChange — Unity 6.0&lt;/a&gt;. The Japanese revision used the Unity 6.0 member page for exception conditions and request behavior, and checked the declaration against the Unity 6.3 class reference and C# reference source. It could not retrieve the 6.3 member page. Behavior in a Unity 6.3 runtime remains untested; this adaptation has not revalidated it.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn27"&gt;
&lt;p&gt;Unity Scripting API, &lt;a href="https://docs.unity3d.com/6000.0/Documentation/ScriptReference/HDROutputSettings.HDRModeChangeRequested.html" rel="noopener noreferrer"&gt;HDROutputSettings.HDRModeChangeRequested — Unity 6.0&lt;/a&gt;. The Japanese revision used the Unity 6.0 member page for exception conditions and checked the property declaration against the Unity 6.3 class reference and C# reference source. This is not a Unity 6.3 runtime test.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn28"&gt;
&lt;p&gt;Unity Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/HDROutputSettings.html" rel="noopener noreferrer"&gt;HDROutputSettings&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn29"&gt;
&lt;p&gt;Unity Technologies, &lt;a href="https://raw.githubusercontent.com/Unity-Technologies/UnityCsReference/6000.3/Runtime/Export/Graphics/Graphics.bindings.cs" rel="noopener noreferrer"&gt;UnityCsReference / 6000.3 / Graphics.bindings.cs&lt;/a&gt;. Used in the Japanese revision to check public property and method declarations in HDROutputSettings, not native implementation details or runtime behavior.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn30"&gt;
&lt;p&gt;Unity Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/HDROutputSettings-paperWhiteNits.html" rel="noopener noreferrer"&gt;HDROutputSettings.paperWhiteNits — Unity 6.3&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn31"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/post-processing/troubleshoot-hdr-custom-pass.html" rel="noopener noreferrer"&gt;Troubleshoot custom render passes with HDR output in URP&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn32"&gt;
&lt;p&gt;Unity Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/HDROutputSettings-automaticHDRTonemapping.html" rel="noopener noreferrer"&gt;HDROutputSettings.automaticHDRTonemapping — Unity 6.3&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn33"&gt;
&lt;p&gt;Unity Shader Graph 17.3 documentation, &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Saturate-Node.html" rel="noopener noreferrer"&gt;Saturate Node&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn34"&gt;
&lt;p&gt;Unity Shader Graph 17.3 documentation, &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Color-Node.html" rel="noopener noreferrer"&gt;Color Node&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn35"&gt;
&lt;p&gt;Unity Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Experimental.Rendering.GraphicsFormat.html" rel="noopener noreferrer"&gt;GraphicsFormat&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn36"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/post-processing/hdr-output-implement-custom-overlay.html" rel="noopener noreferrer"&gt;Implement a custom HDR overlay in URP&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn37"&gt;
&lt;p&gt;Unity Scripting API, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/ImageConversion.EncodeToEXR.html" rel="noopener noreferrer"&gt;ImageConversion.EncodeToEXR&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn38"&gt;
&lt;p&gt;Unity Manual, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/post-processing/hdr-output-debug-views-urp.html" rel="noopener noreferrer"&gt;HDR Output debug views in URP&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>graphics</category>
      <category>csharp</category>
    </item>
    <item>
      <title>6 Shader Graph Effects in Unity 6.3 You'll Want in Your Game</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Wed, 16 Sep 2026 06:36:06 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/6-shader-graph-effects-in-unity-63-youll-want-in-your-game-5f6</link>
      <guid>https://dev.to/gamedevtoollab/6-shader-graph-effects-in-unity-63-youll-want-in-your-game-5f6</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;An enemy burns away. The edge where a shield meets the ground lights up. The scenery behind a heat source shimmers.&lt;/p&gt;

&lt;p&gt;Effects like these start less with complex textures and more with deciding &lt;strong&gt;which input drives the effect, and which part of the object it changes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this article, we'll build six visually and technically different effects using URP and Shader Graph in Unity 6.3. The sample project is public and includes editable graphs and reusable prefabs. Here's a video showing all six effects:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Xxcu5r0s7iY" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;th&gt;What it looks like&lt;/th&gt;
&lt;th&gt;Core idea&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;01 Emissive dissolve&lt;/td&gt;
&lt;td&gt;An object disappears in patches, leaving a glowing edge&lt;/td&gt;
&lt;td&gt;Threshold a noise pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02 Intersection shield&lt;/td&gt;
&lt;td&gt;A glowing band appears where a shield intersects the ground or an obstacle&lt;/td&gt;
&lt;td&gt;Use screen-space depth differences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03 Heat haze&lt;/td&gt;
&lt;td&gt;The background behind a heat source shimmers&lt;/td&gt;
&lt;td&gt;Offset the coordinates used to sample the background&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04 Triplanar snow&lt;/td&gt;
&lt;td&gt;Texture a rock without UV unwrapping and cover its upward-facing surfaces with snow&lt;/td&gt;
&lt;td&gt;Use projection coordinates and surface orientation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05 Interactive grass&lt;/td&gt;
&lt;td&gt;Grass bends in the wind and around an approaching character while its roots stay fixed&lt;/td&gt;
&lt;td&gt;Displace vertices according to their height&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;06 Scan pulse&lt;/td&gt;
&lt;td&gt;A glowing band travels continuously across floors, walls, and buildings&lt;/td&gt;
&lt;td&gt;Use distance in world space&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The graphs, materials, required meshes and textures, and control scripts are grouped under Runtime. Each effect has also been tested when imported into a separate project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open the sample project
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques" rel="noopener noreferrer"&gt;Public repository / latest version&lt;/a&gt; · &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/tree/846cb951577036ccbfb7ea24cf2568582f2ae1f5" rel="noopener noreferrer"&gt;Pinned commit used for this article&lt;/a&gt; · &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/blob/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Assets/ShaderGraphTechniques/Demo/Scenes/00_Gallery.unity" rel="noopener noreferrer"&gt;Gallery scene&lt;/a&gt; · &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/blob/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Documentation/VALIDATION.md" rel="noopener noreferrer"&gt;Validation record&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use Unity Hub to open the repository folder with Unity &lt;strong&gt;&lt;code&gt;6000.3.22f1&lt;/code&gt;&lt;/strong&gt;. After package resolution and compilation finish, open &lt;code&gt;Assets/ShaderGraphTechniques/Demo/Scenes/00_Gallery.unity&lt;/code&gt; and enter Play mode. Implementation links throughout this article point to the pinned commit.&lt;/p&gt;

&lt;p&gt;The graphs, materials, prefabs, and control scripts for all six effects are under &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/tree/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Assets/ShaderGraphTechniques/Runtime/" rel="noopener noreferrer"&gt;Assets/ShaderGraphTechniques/Runtime/&lt;/a&gt;. Each effect's &lt;code&gt;GETTING_STARTED_JA.md&lt;/code&gt; lists its dependencies and setup requirements. Open the included &lt;code&gt;.shadergraph&lt;/code&gt; in the Graph Editor to inspect the actual connections.&lt;/p&gt;

&lt;p&gt;The video-selection and publication-status notes in the pinned commit are outdated. For the video, use the link at the beginning of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Target environment
&lt;/h3&gt;

&lt;p&gt;Unity 6.3 belongs to the &lt;code&gt;6000.3&lt;/code&gt; release family. Its official manual links to the Shader Graph 17.3 package documentation for this generation of features. This article targets the tested combination of &lt;code&gt;6000.3.22f1&lt;/code&gt; and URP / Shader Graph &lt;code&gt;17.3.0&lt;/code&gt;. This does &lt;strong&gt;not&lt;/strong&gt; mean that all six effects became possible for the first time in Unity 6.3.&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Assumption in this article&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Editor&lt;/td&gt;
&lt;td&gt;Unity 6.3 LTS / &lt;code&gt;6000.3.22f1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render pipeline&lt;/td&gt;
&lt;td&gt;URP &lt;code&gt;17.3.0&lt;/code&gt; / Shader Graph &lt;code&gt;17.3.0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Renderer&lt;/td&gt;
&lt;td&gt;Universal Renderer / Forward / standard Render Graph path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Color space&lt;/td&gt;
&lt;td&gt;Linear&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Camera&lt;/td&gt;
&lt;td&gt;A single Base Camera; start with Perspective&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graph precision&lt;/td&gt;
&lt;td&gt;Single as the baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target renderer&lt;/td&gt;
&lt;td&gt;A regular MeshRenderer; HDRP, Built-in, and the 2D Renderer are outside this article's scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capture environment&lt;/td&gt;
&lt;td&gt;Windows 64-bit, NVIDIA GeForce RTX 3060 Laptop GPU, Direct3D 12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The sample includes &lt;code&gt;ProjectSettings/ProjectVersion.txt&lt;/code&gt;, &lt;code&gt;Packages/manifest.json&lt;/code&gt;, and &lt;code&gt;Packages/packages-lock.json&lt;/code&gt;. Open it with this combination first rather than swapping in a different generation of URP or Shader Graph. When moving an effect into your own project, check the versions resolved by Package Manager there as well.&lt;/p&gt;

&lt;p&gt;Distances and displacements in the sample scenes are tuned for 1 Unity unit = 1 m. Adjust distance-related properties when moving an effect into a game with a different scale.&lt;/p&gt;

&lt;p&gt;These examples do not require a custom Renderer Feature or a hand-written shader implementation. Shader Graph handles the visuals; the runtime C# code handles parameter control, material ownership, and grass bounds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Depth Texture and Opaque Texture are different settings
&lt;/h3&gt;

&lt;p&gt;In the URP Asset, &lt;code&gt;Depth Texture&lt;/code&gt; enables depth sampling, while &lt;code&gt;Opaque Texture&lt;/code&gt; provides the background color after opaque objects have been rendered. The camera can override these settings.&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;th&gt;Depth Texture&lt;/th&gt;
&lt;th&gt;Opaque Texture&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Intersection shield&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;Not required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heat haze&lt;/td&gt;
&lt;td&gt;Not required for the basic version&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other effects&lt;/td&gt;
&lt;td&gt;Not required for the effect's calculations&lt;/td&gt;
&lt;td&gt;Not required&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The sample enables both, but its project settings are not included in the exported packages. Enable only the settings needed by the effect in the destination project. Check not just Graphics settings, but also which URP Asset and Renderer the active Quality level and camera actually use.&lt;/p&gt;

&lt;p&gt;For the heat-haze comparison, start with &lt;code&gt;Opaque Downsampling = None&lt;/code&gt;. A downsampled background texture changes sharpness independently of the distortion itself.&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Adjust bright colors and Bloom separately
&lt;/h3&gt;

&lt;p&gt;To create an emissive-looking area, feed bright values into &lt;code&gt;Emission&lt;/code&gt; in a Lit Graph or &lt;code&gt;Base Color&lt;/code&gt; in an Unlit Graph. &lt;strong&gt;Bloom is what makes that brightness spread into the surrounding image.&lt;/strong&gt;&lt;sup id="fnref3"&gt;3&lt;/sup&gt;&lt;sup id="fnref4"&gt;4&lt;/sup&gt;&lt;sup id="fnref5"&gt;5&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;When using Bloom, check the URP and camera HDR settings, the camera's &lt;code&gt;Post Processing&lt;/code&gt; setting, the Global Volume and its Bloom override, and the relationship between the camera's Volume Mask and the Volume's Layer. Simply adding a Volume is not enough.&lt;sup id="fnref6"&gt;6&lt;/sup&gt;&lt;sup id="fnref7"&gt;7&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Make each effect readable without Bloom first, then add a subtle amount. The minimal comparison scenes use no Fog and do not require TAA, motion blur, or dynamic resolution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read the formulas as node connections
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;text&lt;/code&gt; blocks below are &lt;strong&gt;compact descriptions of node connections&lt;/strong&gt;, not HLSL you can paste and execute.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Saturate&lt;/code&gt; clamps a value to the 0–1 range. &lt;code&gt;Step(edge, x)&lt;/code&gt; returns 0 when &lt;code&gt;x &amp;lt; edge&lt;/code&gt;, and 1 otherwise. &lt;code&gt;Smoothstep(a, b, x)&lt;/code&gt; smoothly transitions from 0 to 1 as &lt;code&gt;x&lt;/code&gt; moves from &lt;code&gt;a&lt;/code&gt; to &lt;code&gt;b&lt;/code&gt;. Throughout this article, we keep &lt;code&gt;a &amp;lt; b&lt;/code&gt;.&lt;sup id="fnref8"&gt;8&lt;/sup&gt;&lt;sup id="fnref9"&gt;9&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Pws&lt;/code&gt; means &lt;code&gt;Position / World&lt;/code&gt;, and &lt;code&gt;PosOS&lt;/code&gt; means &lt;code&gt;Position / Object&lt;/code&gt;. Notation such as &lt;code&gt;.xz&lt;/code&gt; means extracting the required components with &lt;code&gt;Split&lt;/code&gt; and &lt;code&gt;Combine&lt;/code&gt;, or with &lt;code&gt;Swizzle&lt;/code&gt;. We use a consistent world-space coordinate system within URP.&lt;sup id="fnref10"&gt;10&lt;/sup&gt;&lt;sup id="fnref11"&gt;11&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;For each public property, set a clear display name and a &lt;strong&gt;Reference name&lt;/strong&gt; matching the identifier used here, such as &lt;code&gt;_Progress&lt;/code&gt;. In Shader Graph 17.3, the setting that exposes a property in the material Inspector is named &lt;code&gt;Show In Inspector&lt;/code&gt;.&lt;sup id="fnref12"&gt;12&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;In effects 01–04 and 06 at the pinned commit, &lt;code&gt;Position / Object → Vertex / Position&lt;/code&gt; is explicitly connected even though those graphs do not deform vertices. The Windows StrictMode build passed with that connection in place, so use the same connection when building the graphs manually. The &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/blob/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Documentation/VALIDATION.md" rel="noopener noreferrer"&gt;validation record&lt;/a&gt; explains the Motion Vector pass fix behind it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Give animated effects a time input you can pause
&lt;/h4&gt;

&lt;p&gt;In effects 03 and 05, &lt;code&gt;t&lt;/code&gt; normally comes from the &lt;code&gt;Time&lt;/code&gt; output of the &lt;code&gt;Time&lt;/code&gt; node. The sample puts the following expression in &lt;code&gt;Runtime/Common/ManualTime.shadersubgraph&lt;/code&gt;, allowing an effect to be frozen at a chosen time for capture or comparison.&lt;sup id="fnref13"&gt;13&lt;/sup&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t = Lerp(Time.Time, _ManualTime, Saturate(_UseManualTime))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_UseManualTime&lt;/code&gt; is a Float in the 0–1 range, with a default of 0. &lt;code&gt;_ManualTime&lt;/code&gt; is a Float measured in seconds, also defaulting to 0. The shared Sub Graph has explicit inputs and does not depend on a capture-only global variable. The property tables below are starting values for building the graphs manually; they are not values held constant throughout every frame of the video.&lt;/p&gt;

&lt;h2&gt;
  
  
  01 — Dissolve with a glowing edge
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=Xxcu5r0s7iY&amp;amp;t=3s" rel="noopener noreferrer"&gt;Watch this effect (0:03)&lt;/a&gt; · &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/tree/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Assets/ShaderGraphTechniques/Runtime/01_Dissolve/" rel="noopener noreferrer"&gt;Implementation folder at the pinned commit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The graph is &lt;code&gt;SG_01_Dissolve.shadergraph&lt;/code&gt;. Pass a value from 0 to 1 to the prefab's &lt;code&gt;DissolveController.SetProgress(value)&lt;/code&gt; to control it. To use it on your own MeshRenderer with UV0, add &lt;code&gt;MaterialInstanceOwner&lt;/code&gt; and &lt;code&gt;DissolveController&lt;/code&gt; to the same GameObject.&lt;/p&gt;

&lt;p&gt;This works well for enemy deaths, summoning, and item appearances. Instead of lowering the opacity of the entire object, it removes parts of the surface according to a noise pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Graph settings and properties
&lt;/h3&gt;

&lt;p&gt;Create a URP Lit Graph with &lt;code&gt;Surface Type = Opaque&lt;/code&gt; and &lt;code&gt;Alpha Clipping = On&lt;/code&gt;. Alpha Clipping discards pixels whose Alpha is below a threshold.&lt;sup id="fnref3"&gt;3&lt;/sup&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reference&lt;/th&gt;
&lt;th&gt;Type and starting value&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;_Progress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 0–1; default 0&lt;/td&gt;
&lt;td&gt;Fully visible at 0, fully removed at 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_NoiseScale&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 8&lt;/td&gt;
&lt;td&gt;Noise pattern density&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_EdgeWidth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 0.06&lt;/td&gt;
&lt;td&gt;Width of the glowing band&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_BaseColor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Color; your choice of surface color&lt;/td&gt;
&lt;td&gt;Normal surface color&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_EdgeColor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HDR Color; orange&lt;/td&gt;
&lt;td&gt;Glow color&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_EdgeIntensity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 3&lt;/td&gt;
&lt;td&gt;Glow intensity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start with a Sphere or Capsule that has UV0. Because this version uses UVs, a mesh without UVs—or with heavily overlapping UVs—will not produce the intended pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core connections
&lt;/h3&gt;

&lt;p&gt;Feed UV0's XY components and &lt;code&gt;_NoiseScale&lt;/code&gt; into &lt;code&gt;Simple Noise&lt;/code&gt;. Pass its output through &lt;code&gt;Saturate&lt;/code&gt; and call the result &lt;code&gt;m&lt;/code&gt;.&lt;sup id="fnref14"&gt;14&lt;/sup&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;p       = Saturate(_Progress)
w       = Max(_EdgeWidth, 0.0001)
m       = Saturate(SimpleNoise(UV0.xy, _NoiseScale))

cut     = Lerp(-w - 0.0001, 1.0001, p)
visible = Step(cut, m)
edge    = visible * (1 - Smoothstep(cut, cut + w, m))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connect &lt;code&gt;cut&lt;/code&gt; to the &lt;code&gt;Edge&lt;/code&gt; input of &lt;code&gt;Step&lt;/code&gt;, and &lt;code&gt;m&lt;/code&gt; to &lt;code&gt;In&lt;/code&gt;. The glowing band occupies a width of &lt;code&gt;w&lt;/code&gt; on the surviving side of the boundary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Simple Noise ── Saturate ──┬─ Step ─────────────── Alpha
                          │
                          └─ Smoothstep → One Minus
                                      × visible
                                      × HDR Color
                                      × Intensity ── Emission
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connect the Fragment outputs as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base Color           = _BaseColor.rgb
Emission             = _EdgeColor.rgb * _EdgeIntensity * edge
Alpha                = visible
Alpha Clip Threshold = 0.5
Metallic             = 0
Smoothness           = 0.35
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why extend the threshold slightly beyond the endpoints?
&lt;/h3&gt;

&lt;p&gt;Using only &lt;code&gt;Step(_Progress, m)&lt;/code&gt; leaves awkward endpoint behavior. In particular, a point where &lt;code&gt;m = 1&lt;/code&gt; can remain visible even at &lt;code&gt;_Progress = 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here, the threshold starts at &lt;code&gt;-w - ε&lt;/code&gt; and ends at &lt;code&gt;1 + ε&lt;/code&gt;. With &lt;code&gt;m&lt;/code&gt; constrained to 0–1, this guarantees &lt;strong&gt;no clipping or glowing edge at the start, and complete removal at the end&lt;/strong&gt;. Those endpoints follow directly from the formula above. Progress is not, however, the percentage of surface area that has disappeared.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integration notes
&lt;/h3&gt;

&lt;p&gt;Clipping in this graph is a visual operation. Control Colliders and gameplay state separately. Check shadows with your actual lights and a Player build so that the object's shadow does not remain after its surface has disappeared.&lt;/p&gt;

&lt;p&gt;If the glow clips to white, lower &lt;code&gt;_EdgeIntensity&lt;/code&gt; first rather than changing Bloom, and inspect the shape of the boundary. This example is intended to work with Opaque + Alpha Clipping, not by switching to transparent blending to hide problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  02 — A shield that glows where it intersects the environment
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=Xxcu5r0s7iY&amp;amp;t=13s" rel="noopener noreferrer"&gt;Watch this effect (0:13)&lt;/a&gt; · &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/tree/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Assets/ShaderGraphTechniques/Runtime/02_IntersectionShield/" rel="noopener noreferrer"&gt;Implementation folder at the pinned commit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The graph is &lt;code&gt;SG_02_IntersectionShield.shadergraph&lt;/code&gt;. It needs no dedicated runtime controller; adjust its exposed material properties. The video hides the two static blue comparison objects and shows the shield and moving pillar. Those comparison objects remain in the technical test scene, so its visible object count differs from the video's.&lt;/p&gt;

&lt;p&gt;Place a transparent sphere so that the floor or a pillar intersects it. A glowing band around that intersection makes it look more like an energy field than a plain transparent sphere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Graph settings and properties
&lt;/h3&gt;

&lt;p&gt;Use a URP Unlit Graph with the following settings. Transparent blending, rendered faces, and depth writing are configured in the Graph Inspector.&lt;sup id="fnref4"&gt;4&lt;/sup&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Surface Type  = Transparent
Blending Mode = Alpha
Render Face   = Front
Depth Write   = Force Disabled
Depth Test    = L Equal
Cast Shadows  = Off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start with the camera outside the sphere. The basic version avoids double-sided rendering because it adds overlapping transparent front and back surfaces.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reference&lt;/th&gt;
&lt;th&gt;Type and starting value&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;_ContactWidth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 0.15&lt;/td&gt;
&lt;td&gt;Depth-difference range that produces the glow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_RimPower&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 4&lt;/td&gt;
&lt;td&gt;Rim sharpness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_RimIntensity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 2&lt;/td&gt;
&lt;td&gt;Rim brightness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_ContactIntensity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 5&lt;/td&gt;
&lt;td&gt;Intersection-band brightness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_ShieldColor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HDR Color; blue-green&lt;/td&gt;
&lt;td&gt;Shield color&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_Opacity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 0–1; default 1&lt;/td&gt;
&lt;td&gt;Visibility&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Use the dedicated depth-difference node
&lt;/h3&gt;

&lt;p&gt;Add &lt;code&gt;Scene Depth Difference&lt;/code&gt; and set its Sampling Mode to &lt;code&gt;Eye&lt;/code&gt;. Connect &lt;code&gt;Screen Position / Default&lt;/code&gt; to &lt;code&gt;Scene UV&lt;/code&gt;, and &lt;code&gt;Position / World&lt;/code&gt; to &lt;code&gt;Position WS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This node returns the difference between the sampled scene depth and the depth of the supplied world-space position. Eye mode uses meters, and the result is negative when the sampled surface is closer to the camera.&lt;sup id="fnref15"&gt;15&lt;/sup&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;d       = SceneDepthDifference(Eye)
w       = Max(_ContactWidth, 0.0001)
contact = Step(0, d) * (1 - Saturate(d / w))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;contact&lt;/code&gt; approaches 1 as the depths get closer and falls to 0 as they separate. This measures depth along the camera's viewing direction: &lt;strong&gt;it is not the shortest distance between objects or a Collider contact test&lt;/strong&gt;. Changing the view changes the appearance of the band.&lt;/p&gt;

&lt;p&gt;Add a &lt;code&gt;Fresnel Effect&lt;/code&gt; node as well. Keep Normal and View Direction in the same World space, and pass &lt;code&gt;_RimPower&lt;/code&gt; to Power. Call the output &lt;code&gt;rim&lt;/code&gt;. Fresnel Effect emphasizes surfaces seen at a grazing angle.&lt;sup id="fnref16"&gt;16&lt;/sup&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base Color = _ShieldColor.rgb
             * (0.2 + rim * _RimIntensity
                    + contact * _ContactIntensity)

Alpha = Saturate((0.04 + 0.25 * rim + 0.8 * contact) * _Opacity)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What to check when the intersection does not glow
&lt;/h3&gt;

&lt;p&gt;First, make sure Depth Texture is enabled for the active URP Asset and camera. Then give the intersecting floor or pillar an ordinary opaque material, such as URP/Lit.&lt;/p&gt;

&lt;p&gt;This technique can only read surfaces present in the depth texture at that point in rendering. It does not detect intersections with every transparent object. Do not treat enabling Depth Write on the shield itself as a fix.&lt;/p&gt;

&lt;p&gt;Also place an opaque box in front of the camera and check that it hides the shield behind it as usual. Changing the depth test to &lt;code&gt;Always&lt;/code&gt; would turn this into a different, see-through effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  03 — Heat haze that distorts the background
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=Xxcu5r0s7iY&amp;amp;t=23s" rel="noopener noreferrer"&gt;Watch this effect (0:23)&lt;/a&gt; · &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/tree/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Assets/ShaderGraphTechniques/Runtime/03_HeatHaze/" rel="noopener noreferrer"&gt;Implementation folder at the pinned commit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The graph is &lt;code&gt;SG_03_HeatHaze.shadergraph&lt;/code&gt;. The prefab's &lt;code&gt;HeatHazeController&lt;/code&gt; provides &lt;code&gt;SetManualTime(seconds)&lt;/code&gt;, &lt;code&gt;UseRealtime()&lt;/code&gt;, and &lt;code&gt;SetOpacity(0)&lt;/code&gt; for a frozen comparison, normal time, and hiding the effect.&lt;/p&gt;

&lt;p&gt;This heat shimmer can be used above flames, exhaust vents, magic circles, or hot ground.&lt;/p&gt;

&lt;p&gt;Rather than overlaying a semitransparent color, it &lt;strong&gt;resamples the background image at slightly offset coordinates&lt;/strong&gt; to make the air appear to shimmer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Graph settings and properties
&lt;/h3&gt;

&lt;p&gt;Use a URP Unlit Graph with &lt;code&gt;Transparent / Alpha&lt;/code&gt;, &lt;code&gt;Depth Write = Force Disabled&lt;/code&gt;, &lt;code&gt;Depth Test = L Equal&lt;/code&gt;, and &lt;code&gt;Cast Shadows = Off&lt;/code&gt;. Start by applying it to a Quad whose front face points toward the camera.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reference&lt;/th&gt;
&lt;th&gt;Type and starting value&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;_NoiseScale&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 6&lt;/td&gt;
&lt;td&gt;Distortion pattern density&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_Distortion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 0.01&lt;/td&gt;
&lt;td&gt;Distortion amount in normalized screen UVs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_Opacity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 0–1; default 1&lt;/td&gt;
&lt;td&gt;Blend amount for the distorted background&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_FlowA&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Vector2, (0.12, 0.20)&lt;/td&gt;
&lt;td&gt;Motion of the first noise pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_FlowB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Vector2, (-0.18, 0.10)&lt;/td&gt;
&lt;td&gt;Motion of the second noise pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_ScreenEdgeFade&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 0.04&lt;/td&gt;
&lt;td&gt;Width over which distortion fades near screen edges&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Also add the shared &lt;code&gt;_UseManualTime&lt;/code&gt; and &lt;code&gt;_ManualTime&lt;/code&gt; properties.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use the Quad's UVs and screen UVs for different jobs
&lt;/h3&gt;

&lt;p&gt;Build the distortion pattern from the Quad's UV0, but use the XY components of &lt;code&gt;Screen Position / Default&lt;/code&gt; to sample the background. Default provides normalized 0–1 screen coordinates; it is not the same as Raw.&lt;sup id="fnref17"&gt;17&lt;/sup&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;u = UV0.xy
s = ScreenPosition(Default).xy

n1 = SimpleNoise(u * _NoiseScale + t * _FlowA, Scale = 1)
n2 = SimpleNoise(u * _NoiseScale + t * _FlowB + (19.7, 3.1), Scale = 1)
v  = 2 * Vector2(n1, n2) - Vector2(1, 1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two noise values form &lt;code&gt;v&lt;/code&gt;, which supplies the two components of the background sampling offset.&lt;/p&gt;

&lt;p&gt;Next, build a mask that softens the Quad's perimeter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;r        = Length(u * 2 - 1)
shape    = 1 - Smoothstep(0.65, 1, r)
edgeDist = Min(Min(s.x, 1 - s.x), Min(s.y, 1 - s.y))
screen   = Smoothstep(0, Max(_ScreenEdgeFade, 0.0001), edgeDist)

sampleUV = Clamp(s + v * _Distortion * shape * screen,
                 (0.001, 0.001), (0.999, 0.999))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;shape&lt;/code&gt; hides the Quad's square boundary, and &lt;code&gt;screen&lt;/code&gt; reduces distortion near the screen edges. The Clamp limits are fixed margins for this basic version, not an exact half-texel correction.&lt;/p&gt;

&lt;p&gt;Pass &lt;code&gt;sampleUV&lt;/code&gt; into the XY components of the &lt;code&gt;Scene Color&lt;/code&gt; UV input, and use the output directly as the color. If you construct a Vector4, set the unused ZW components to 0.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base Color = SceneColor(sampleUV)
Alpha      = shape * Saturate(_Opacity)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not multiply RGB by Alpha beforehand; let Alpha blending perform the compositing. This example also does not multiply Scene Color by an HDR intensity to make it glow.&lt;/p&gt;

&lt;h3&gt;
  
  
  There are limits to the background this method can read
&lt;/h3&gt;

&lt;p&gt;In URP, Scene Color reads the Opaque Texture copied before transparent objects are rendered. Use it in the Fragment stage. It is not a way to freely distort the entire rendered frame, including glass and particles.&lt;sup id="fnref18"&gt;18&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;As a result, overlapping transparent objects or multiple heat-haze surfaces can look as though they overwrite transparent effects drawn earlier. Changing draw order cannot recover information absent from the background copy.&lt;/p&gt;

&lt;p&gt;Because this only offsets screen-space sample positions, it can also pull in colors from other objects near foreground silhouettes. Screen-edge fading and Clamp do not solve every silhouette artifact.&lt;/p&gt;

&lt;p&gt;Evaluate the basic version without Fog, against an opaque background, using a single Quad. To compare with the effect disabled, use &lt;code&gt;_Opacity = 0&lt;/code&gt; or disable the Renderer. &lt;strong&gt;Setting distortion to 0 does not stop the shader from drawing the background copy.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  04 — Triplanar rocks with snow on upward-facing surfaces
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=Xxcu5r0s7iY&amp;amp;t=33s" rel="noopener noreferrer"&gt;Watch this effect (0:33)&lt;/a&gt; · &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/tree/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Assets/ShaderGraphTechniques/Runtime/04_TriplanarSnow/" rel="noopener noreferrer"&gt;Implementation folder at the pinned commit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The graph is &lt;code&gt;SG_04_TriplanarSnow.shadergraph&lt;/code&gt;. It needs no dedicated runtime controller. Adjust &lt;code&gt;_SnowAmount&lt;/code&gt; on the material, and try replacing the included rock texture with your own.&lt;/p&gt;

&lt;p&gt;This effect textures rocks and cliffs without well-organized UVs, then spreads snow across them. Changing the snow color to green or dark brown also makes it useful for moss or dirt coverage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Graph settings and properties
&lt;/h3&gt;

&lt;p&gt;Use a URP Lit Graph with &lt;code&gt;Surface Type = Opaque&lt;/code&gt; and &lt;code&gt;Alpha Clipping = Off&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reference&lt;/th&gt;
&lt;th&gt;Type and starting value&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;_BaseMap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Texture2D&lt;/td&gt;
&lt;td&gt;A tileable rock color texture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_BaseTint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Color; white&lt;/td&gt;
&lt;td&gt;Rock color tint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_TextureScale&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 1&lt;/td&gt;
&lt;td&gt;Texture density in world space&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_SnowAmount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 0–1; default 0.5&lt;/td&gt;
&lt;td&gt;Snow coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_SnowColor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Color; slightly blue-tinted white&lt;/td&gt;
&lt;td&gt;Snow color&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_SnowNoiseScale&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 1.5&lt;/td&gt;
&lt;td&gt;Coverage pattern density&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_SnowSoftness&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 0.12&lt;/td&gt;
&lt;td&gt;Softness of the snow boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Import &lt;code&gt;_BaseMap&lt;/code&gt; as a color texture with sRGB enabled, Wrap Mode set to Repeat, and Mip Maps enabled.&lt;sup id="fnref19"&gt;19&lt;/sup&gt; The sample includes a custom, pre-generated texture named &lt;code&gt;TEX_SeamlessRock.png&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Texture the rock with Triplanar
&lt;/h3&gt;

&lt;p&gt;Set the &lt;code&gt;Triplanar&lt;/code&gt; node's Type to &lt;code&gt;Default&lt;/code&gt; and Input Space to &lt;code&gt;World&lt;/code&gt;. Connect &lt;code&gt;_BaseMap&lt;/code&gt; to Texture and &lt;code&gt;_TextureScale&lt;/code&gt; to Tile, and start with Blend at 4. Keep Position and Normal in World space as well.&lt;/p&gt;

&lt;p&gt;Triplanar projects a texture from three directions and blends the results according to surface orientation. It samples the input texture three times, so its cost is not the same as a single ordinary texture lookup. Use this node in the Fragment stage.&lt;sup id="fnref20"&gt;20&lt;/sup&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rock = Triplanar(_BaseMap,
                 Position = Pws,
                 Normal   = Normalize(NormalVector(World)),
                 Tile     = _TextureScale,
                 Blend    = 4).rgb * _BaseTint.rgb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Multiply upward-facing slope by coverage
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;Normal Vector / World&lt;/code&gt; for surface orientation. Take its Dot Product with &lt;code&gt;(0, 1, 0)&lt;/code&gt; to measure how much it faces upward.&lt;sup id="fnref21"&gt;21&lt;/sup&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;up    = Dot(Normalize(NormalVector(World)), (0, 1, 0))
slope = Smoothstep(0.35, 0.85, up)

n     = Saturate(SimpleNoise(Pws.xz, _SnowNoiseScale))
a     = Saturate(_SnowAmount)
s     = Max(_SnowSoftness, 0.0001)
cut   = Lerp(1 + s + 0.0001, -s - 0.0001, a)
cover = Smoothstep(cut - s, cut + s, n)
snow  = slope * cover
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;slope&lt;/code&gt; limits which surfaces can receive snow, and the noise-based &lt;code&gt;cover&lt;/code&gt; fills those surfaces. With this formula, Snow Amount 0 means no snow; at 1, coverage reaches the limit allowed by &lt;code&gt;slope&lt;/code&gt;. Even at 1, vertical walls do not turn completely white.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base Color = Lerp(rock, _SnowColor.rgb, snow)
Smoothness = Lerp(0.2, 0.35, snow)
Metallic   = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  This is the appearance of snow, not a volume simulation
&lt;/h3&gt;

&lt;p&gt;The basic version changes color and surface appearance only. It does not thicken the silhouette with snow or detect areas sheltered by a roof. An upward-facing surface can receive snow even indoors.&lt;/p&gt;

&lt;p&gt;Because the projection is in world space, moving the object changes its position relative to the pattern. This makes the setup suitable for stationary rocks and buildings. For a moving prop that needs the pattern to stay attached, redesign the projection coordinates in Object space. The normal test that restricts snow to upward-facing surfaces can still remain in World space.&lt;/p&gt;

&lt;p&gt;“No UVs required” does not mean “no inputs or conditions required.” The texture's Repeat setting, the normals, and the projection space are part of this effect's input requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  05 — Grass that bends around an approaching character
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=Xxcu5r0s7iY&amp;amp;t=43s" rel="noopener noreferrer"&gt;Watch this effect (0:43)&lt;/a&gt; · &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/tree/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Assets/ShaderGraphTechniques/Runtime/05_InteractiveGrass/" rel="noopener noreferrer"&gt;Implementation folder at the pinned commit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The graph is &lt;code&gt;SG_05_InteractiveGrass.shadergraph&lt;/code&gt;. Assign the target Transform to the prefab's &lt;code&gt;InteractiveGrassController.Interactor&lt;/code&gt;. Use &lt;code&gt;SetBend(radius, strength)&lt;/code&gt; to adjust interaction bending, &lt;code&gt;SetManualTime(seconds)&lt;/code&gt; for a frozen comparison, and &lt;code&gt;UseRealtime()&lt;/code&gt; to return to normal time.&lt;/p&gt;

&lt;p&gt;This adds bending around an approaching character to grass that already sways in the wind.&lt;/p&gt;

&lt;p&gt;The central idea is to &lt;strong&gt;keep root vertices fixed and increase displacement toward each blade's tip&lt;/strong&gt;. This changes the Vertex stage rather than just the surface color.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define the mesh requirements first
&lt;/h3&gt;

&lt;p&gt;In the basic version, narrow polygons form the grass silhouette. No grass image is used for Alpha Clipping.&lt;/p&gt;

&lt;p&gt;Make each blade a tapered strip with roughly six subdivisions along its height. Set UV0's Y component to 0 at the root and 1 at the tip. Even when multiple blades share one mesh, each blade needs its own 0–1 range in UV0.Y.&lt;/p&gt;

&lt;p&gt;A single unsubdivided Quad cannot form a curve along its length. The sample's &lt;code&gt;MESH_GrassCluster.asset&lt;/code&gt; is already generated, with six vertical subdivisions per blade. You do not need to run a generation script in the destination project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Graph settings and properties
&lt;/h3&gt;

&lt;p&gt;Use a URP Unlit Graph with &lt;code&gt;Opaque&lt;/code&gt;, &lt;code&gt;Alpha Clipping = Off&lt;/code&gt;, and &lt;code&gt;Render Face = Both&lt;/code&gt;. Because this basic version does not calculate lighting, we can leave reconstructing deformed normals out of the example.&lt;sup id="fnref4"&gt;4&lt;/sup&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reference&lt;/th&gt;
&lt;th&gt;Type and starting value&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;_InteractorPositionWS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Vector3, (0, 0, 0)&lt;/td&gt;
&lt;td&gt;World position of the object pushing the grass aside&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_InteractorEnabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 0–1; default 0&lt;/td&gt;
&lt;td&gt;Stops proximity-based bending at 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_BendRadius&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 1&lt;/td&gt;
&lt;td&gt;Horizontal interaction radius&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_BendStrength&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 0.6&lt;/td&gt;
&lt;td&gt;Interaction displacement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_WindDirectionXZ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Vector2, (1, 0)&lt;/td&gt;
&lt;td&gt;Wind direction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_WindStrength&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 0.08&lt;/td&gt;
&lt;td&gt;Wind displacement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_WindSpeed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 1.5&lt;/td&gt;
&lt;td&gt;Wind animation speed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_RootColor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Color; dark green&lt;/td&gt;
&lt;td&gt;Root color&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_TipColor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Color; light green&lt;/td&gt;
&lt;td&gt;Tip color&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Also add the shared manual-time properties. Distances and displacements are tuned for the sample scenes' 1 Unity unit = 1 m scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build the vertex displacement
&lt;/h3&gt;

&lt;p&gt;Here, &lt;code&gt;Pws&lt;/code&gt; is the undeformed &lt;code&gt;Position / World&lt;/code&gt;, evaluated in the Vertex stage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;h       = Saturate(UV0.y)
rootMask = h * h

q       = Pws.xz - _InteractorPositionWS.xz
d       = Length(q)
outward = q / Max(d, 0.0001)
radius  = Max(_BendRadius, 0.0001)
weight  = (1 - Smoothstep(0, radius, d)) * Saturate(_InteractorEnabled)

windDir = _WindDirectionXZ / Max(Length(_WindDirectionXZ), 0.0001)
phase   = Pws.x * 0.7 + Pws.z * 0.9 + t * _WindSpeed
wind    = windDir * Sin(phase) * _WindStrength

moveXZ  = (outward * weight * _BendStrength + wind) * rootMask
movedWS = Pws + Vector3(moveXZ.x, 0, moveXZ.y)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the root, &lt;code&gt;h * h&lt;/code&gt; is 0, so the vertices stay fixed. Displacement increases toward the tip. The formula avoids division by zero even when a vertex lies exactly at the interaction center and &lt;code&gt;d = 0&lt;/code&gt;. At that point, the outward direction is zero and only wind contributes to displacement.&lt;/p&gt;

&lt;p&gt;Finally, pass &lt;code&gt;movedWS&lt;/code&gt; through &lt;code&gt;Transform / World → Object / Type = Position&lt;/code&gt; and connect the result to the Vertex &lt;code&gt;Position&lt;/code&gt; input.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Position(World) → Add displacement
                → Transform(World → Object, Position)
                → Vertex / Position
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Vertex Position input expects Object space. Do not connect a World-space position directly. The Transform node's &lt;code&gt;Position&lt;/code&gt; type includes translation, unlike &lt;code&gt;Direction&lt;/code&gt;.&lt;sup id="fnref3"&gt;3&lt;/sup&gt;&lt;sup id="fnref11"&gt;11&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;For the Fragment color, start with a simple root-to-tip gradient:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base Color = Lerp(_RootColor.rgb, _TipColor.rgb, Saturate(UV0.y))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The game supplies the interactor's position
&lt;/h3&gt;

&lt;p&gt;On the C# side, pass the target Transform's world position to &lt;code&gt;_InteractorPositionWS&lt;/code&gt;, and set &lt;code&gt;_InteractorEnabled = 1&lt;/code&gt; only while the target is valid. If it is unassigned or has been destroyed, return the value to 0 so that the grass does not react to a nonexistent character at the origin.&lt;/p&gt;

&lt;p&gt;This basic version reacts only to horizontal distance. A character on a bridge at a different height can therefore affect grass below. For multilevel environments, add a height condition or separate the affected grass into groups. This is not physical collision, persistent footprints, or a simulation that preserves blade length.&lt;/p&gt;

&lt;h3&gt;
  
  
  Without correct bounds, grass can disappear at the screen edges
&lt;/h3&gt;

&lt;p&gt;When a shader moves vertices, the bounds used for culling must contain the deformed geometry. Unity's &lt;code&gt;Renderer.localBounds&lt;/code&gt; can be overridden for this purpose. However, that override is not saved into the Scene or Prefab, so it must be applied again at runtime.&lt;sup id="fnref22"&gt;22&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The sample's &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/blob/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Assets/ShaderGraphTechniques/Runtime/05_InteractiveGrass/InteractiveGrassController.cs" rel="noopener noreferrer"&gt;InteractiveGrassController&lt;/a&gt; reapplies bounds when enabled and when &lt;code&gt;SetBend&lt;/code&gt; is called. Its margin comes from &lt;code&gt;boundsPadding&lt;/code&gt;—default 0.9—not from automatically estimating the deformation. Review that margin when increasing bend or wind strength, and reapply the bounds if scale changes at runtime. The bounds must cover the maximum displacement; the Collider is not updated.&lt;/p&gt;

&lt;p&gt;Do not make Static Batching or Dynamic Batching a prerequisite for the first test. In particular, vertex processing that converts back to Object space needs separate checks to ensure batching does not change its coordinate assumptions. Both Static and Dynamic Batching involve combining vertices in world space.&lt;sup id="fnref23"&gt;23&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  06 — A scan pulse that travels across buildings and terrain
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=Xxcu5r0s7iY&amp;amp;t=53s" rel="noopener noreferrer"&gt;Watch this effect (0:53)&lt;/a&gt; · &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/tree/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Assets/ShaderGraphTechniques/Runtime/06_ScanPulse/" rel="noopener noreferrer"&gt;Implementation folder at the pinned commit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The graph is &lt;code&gt;SG_06_ScanPulse.shadergraph&lt;/code&gt;. The prefab's &lt;code&gt;ScanPulseController&lt;/code&gt; provides &lt;code&gt;SetCenter(transform)&lt;/code&gt; and &lt;code&gt;SetState(active, radius)&lt;/code&gt;. When no center is assigned, it uses the component's own position.&lt;/p&gt;

&lt;p&gt;This works for exploration scans, sonar, or magical detection. The same wave should travel across walls and steps as well as the floor.&lt;/p&gt;

&lt;p&gt;The basic version is not a post-processing effect: &lt;strong&gt;the scan pulse is drawn inside the affected materials&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Graph settings and properties
&lt;/h3&gt;

&lt;p&gt;Use a URP Lit Graph with &lt;code&gt;Opaque&lt;/code&gt; and &lt;code&gt;Alpha Clipping = Off&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reference&lt;/th&gt;
&lt;th&gt;Type and starting value&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;_ScanCenterWS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Vector3, (0, 0, 0)&lt;/td&gt;
&lt;td&gt;Wave center&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_ScanRadius&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 0&lt;/td&gt;
&lt;td&gt;Current radius&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_ScanWidth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 0.25&lt;/td&gt;
&lt;td&gt;Half-width of the glowing band&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_ScanActive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 0–1; default 0&lt;/td&gt;
&lt;td&gt;Whether the wave is visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_ScanColor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HDR Color; blue-green&lt;/td&gt;
&lt;td&gt;Wave color&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_ScanIntensity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Float, 3&lt;/td&gt;
&lt;td&gt;Wave brightness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_BaseColor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Color; gray&lt;/td&gt;
&lt;td&gt;Normal surface color&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Light up the area close to a spherical shell
&lt;/h3&gt;

&lt;p&gt;In the Fragment stage, calculate the distance between &lt;code&gt;Position / World&lt;/code&gt; and &lt;code&gt;_ScanCenterWS&lt;/code&gt;.&lt;sup id="fnref10"&gt;10&lt;/sup&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;distanceToCenter = Distance(Pws, _ScanCenterWS)
radius           = Max(_ScanRadius, 0)
width            = Max(_ScanWidth, 0.0001)

distanceToShell  = Abs(distanceToCenter - radius)
ring             = (1 - Smoothstep(0, width, distanceToShell))
                   * Saturate(_ScanActive)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The glow is brightest on the sphere where &lt;code&gt;distanceToCenter = radius&lt;/code&gt;, then fades inward and outward. Because &lt;code&gt;_ScanWidth&lt;/code&gt; applies on both sides of the sphere, the full band with a nonzero value is approximately twice that width.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base Color = _BaseColor.rgb
Emission   = _ScanColor.rgb * _ScanIntensity * ring
Metallic   = 0
Smoothness = 0.25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Increasing &lt;code&gt;_ScanRadius&lt;/code&gt; moves the glowing band through the intersections between that sphere and the floor, walls, and buildings. This uses XYZ distance, not just XZ distance, so it is not the same calculation as projecting a flat circle from the floor onto a wall.&lt;/p&gt;

&lt;h3&gt;
  
  
  Send the same wave parameters to multiple objects
&lt;/h3&gt;

&lt;p&gt;Give every participating Renderer the same center, radius, and width. If the wave lines up where it crosses from the floor to a wall, the coordinate spaces are consistent.&lt;/p&gt;

&lt;p&gt;For a repeating pulse, set &lt;code&gt;_ScanActive = 0&lt;/code&gt; before resetting the radius to 0 to avoid an unwanted flash at the center. To run two independent waves simultaneously, separate the ownership of the materials they control as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not describe this as affecting the entire scene
&lt;/h3&gt;

&lt;p&gt;Materials that do not use this graph will not show the pulse. To add it to an existing, more complex Lit Graph, move the distance-to-&lt;code&gt;ring&lt;/code&gt; calculation into a Sub Graph and add its result to the existing Emission.&lt;/p&gt;

&lt;p&gt;The effect also uses normal depth testing, so it does not reveal objects behind a wall. A full-screen scan or detection display through occluders requires a different rendering design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving the effects into another project
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Keep Runtime separate from Demo
&lt;/h3&gt;

&lt;p&gt;The dependency direction is &lt;code&gt;Demo → Runtime&lt;/code&gt;. Runtime prefabs contain no Camera, Light, Volume, Canvas, or Demo Manager, and they do not require the Gallery. That does not mean the destination scene needs no camera or lighting; it means you do not have to bring along the demonstration setup.&lt;/p&gt;

&lt;p&gt;The reusable parts are &lt;strong&gt;the graph, its referenced Sub Graphs, materials, required textures and meshes, and runtime controllers only where needed&lt;/strong&gt;. The prefabs bundle these into configured objects. Each package also includes a setup guide and a LICENSE file inside Assets.&lt;/p&gt;

&lt;p&gt;Do not copy &lt;code&gt;.shadergraph&lt;/code&gt; or &lt;code&gt;.mat&lt;/code&gt; files arbitrarily through a file explorer. Transfer them in a way that preserves references and &lt;code&gt;.meta&lt;/code&gt; files.&lt;sup id="fnref24"&gt;24&lt;/sup&gt; The sample's &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/blob/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Assets/ShaderGraphTechniques/Editor/ShaderGraphTechniquePackageExporter.cs" rel="noopener noreferrer"&gt;Package Exporter&lt;/a&gt; gathers dependencies, excludes other effects, Demo, Tests, Editor, and ProjectSettings, and passes the resulting set to Unity's Export Package API. Including the required dependencies is not the same as exporting the entire project.&lt;sup id="fnref25"&gt;25&lt;/sup&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;th&gt;What to bring for the visual effect&lt;/th&gt;
&lt;th&gt;Destination requirements&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dissolve&lt;/td&gt;
&lt;td&gt;Graph and material; control code for automated playback&lt;/td&gt;
&lt;td&gt;A mesh with UV0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intersection shield&lt;/td&gt;
&lt;td&gt;Graph, material, and a mesh such as a sphere&lt;/td&gt;
&lt;td&gt;Depth Texture and an opaque intersection target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heat haze&lt;/td&gt;
&lt;td&gt;Graph, time Sub Graph, material, and Quad&lt;/td&gt;
&lt;td&gt;Opaque Texture; an opaque background for the basic version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snow&lt;/td&gt;
&lt;td&gt;Graph, material, and rock texture&lt;/td&gt;
&lt;td&gt;Valid normals, projection coordinates, and Repeat settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grass&lt;/td&gt;
&lt;td&gt;Graph, time Sub Graph, material, dedicated mesh, and position/bounds control&lt;/td&gt;
&lt;td&gt;Root mask in UV0.Y and a target Transform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scan pulse&lt;/td&gt;
&lt;td&gt;Graph and material; control code for automated playback&lt;/td&gt;
&lt;td&gt;Consistent parameters on every participating object&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Generate packages locally, then import them
&lt;/h3&gt;

&lt;p&gt;The six individual packages and &lt;code&gt;ShaderGraphTechniques-Runtime-All.unitypackage&lt;/code&gt; have already been generated, inspected, and tested through isolated imports. However, &lt;strong&gt;the generated archives are excluded from Git&lt;/strong&gt;. Open the original sample project and run &lt;code&gt;Tools &amp;gt; Shader Graph Techniques &amp;gt; Export Runtime Packages&lt;/code&gt; to generate them locally under &lt;code&gt;Artifacts/Packages/&lt;/code&gt;. The &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/blob/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Documentation/DISTRIBUTION.md" rel="noopener noreferrer"&gt;distribution and migration record&lt;/a&gt; lists package names and inspection results.&lt;/p&gt;

&lt;p&gt;Prepare a new URP project in Unity &lt;code&gt;6000.3.22f1&lt;/code&gt; and confirm that URP / Shader Graph resolve to &lt;code&gt;17.3.0&lt;/code&gt;. Use Unity's &lt;code&gt;Assets &amp;gt; Import Package &amp;gt; Custom Package...&lt;/code&gt; to import just the &lt;code&gt;.unitypackage&lt;/code&gt; for the effect you need.&lt;sup id="fnref26"&gt;26&lt;/sup&gt; Enable Depth Texture for effect 02 or Opaque Texture for effect 03 in the destination project, then place the corresponding &lt;code&gt;PF_*.prefab&lt;/code&gt; in the scene or assign &lt;code&gt;MAT_*.mat&lt;/code&gt; to your Renderer.&lt;/p&gt;

&lt;p&gt;Individual packages include the Common files they need. When you import another effect later, shared assets retain the same original GUIDs. You do not need to copy the entire Runtime folder separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Share materials deliberately
&lt;/h3&gt;

&lt;p&gt;When two prefabs are in a scene, changing the Progress of one should not unexpectedly make the other disappear.&lt;/p&gt;

&lt;p&gt;In effects 01, 03, 05, and 06, &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/blob/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Assets/ShaderGraphTechniques/Runtime/Common/MaterialInstanceOwner.cs" rel="noopener noreferrer"&gt;MaterialInstanceOwner&lt;/a&gt; creates an instance for each Renderer material slot and reuses it while initialized. On disable, it restores the original references and destroys only the instances it created. It does not modify shared material assets or global shader values.&lt;/p&gt;

&lt;p&gt;For effects 02 and 04, adjusting a material in its Inspector affects every object sharing that material. To control these independently per prefab, use separate material assets or instances with explicit ownership and cleanup. Distinguish changing a shared material asset from changing one placed object.&lt;/p&gt;

&lt;p&gt;MaterialPropertyBlock is not an automatic optimization. Unity's API documentation explicitly states that it is incompatible with the SRP Batcher. Evaluate ease of per-object control separately from CPU rendering cost.&lt;sup id="fnref27"&gt;27&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Isolated imports were tested in seven independent projects
&lt;/h3&gt;

&lt;p&gt;The six individual packages and Runtime-All were imported separately into &lt;strong&gt;seven independent, newly created URP projects&lt;/strong&gt;, without copying the original project's &lt;code&gt;Assets&lt;/code&gt; or &lt;code&gt;Library&lt;/code&gt; directories. &lt;strong&gt;All seven imports passed (7/7).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without the Gallery or demo Manager, the tests checked prefabs, graphs, supported shaders, and &lt;strong&gt;actual Unity renders of states A/B controlled through the APIs or public material values, image differences, zero missing scripts, and no magenta fallback&lt;/strong&gt;. In the Runtime-All project, all six effects were rendered individually. The package archives themselves were also checked against the manifest for every pathname, GUID, and asset SHA-256, with &lt;strong&gt;7/7 content checks passing&lt;/strong&gt;. These results are documented in the &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/blob/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Documentation/VALIDATION.md" rel="noopener noreferrer"&gt;validation record&lt;/a&gt; and &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/blob/846cb951577036ccbfb7ea24cf2568582f2ae1f5/Documentation/DISTRIBUTION.md" rel="noopener noreferrer"&gt;distribution and migration record&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Separately, a fresh clone was reimported without a Library directory. Package resolution for all 57 packages and script compilation succeeded, followed by EditMode 9/9 and GPU-rendered PlayMode 4/4. Attempts that failed to render with Null Graphics are not counted as successful results.&lt;/p&gt;

&lt;h3&gt;
  
  
  What remains untested
&lt;/h3&gt;

&lt;p&gt;The tested GPU and Graphics API are the RTX 3060 Laptop GPU and Direct3D 12 listed earlier. XR, mobile, macOS / Linux, other GPUs, Direct3D 11 / Vulkan / Metal, and real-time 60 fps performance remain untested. Multiple cameras, Camera Stacks, Fog, Heat Haze compositing with transparent objects, and long-running integration into a real game are also untested. Zero, negative, and mirrored scales are outside the supported scope. Successful isolated imports do not guarantee support across all those environments.&lt;/p&gt;

&lt;p&gt;Measure rendering cost with the target GPU, resolution, Graphics API, and number of visible instances held consistent. Do not call an effect lightweight just because it has few nodes. Background copies, depth sampling, overlapping transparent surfaces, and Triplanar's multiple texture samples are different sources of cost.&lt;/p&gt;

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

&lt;p&gt;Rather than remembering these as six unrelated tricks, focus on the inputs they use.&lt;/p&gt;

&lt;p&gt;Dissolve and snow turn values into &lt;strong&gt;regions to reveal or cover&lt;/strong&gt;. The shield and heat haze read &lt;strong&gt;screen information from the camera&lt;/strong&gt;. Grass and the scan pulse use &lt;strong&gt;positions and distances&lt;/strong&gt; to drive their changes.&lt;/p&gt;

&lt;p&gt;In the fixed environment, the sample passed EditMode 9/9, PlayMode 4/4, a Windows StrictMode build with 0 errors / 0 warnings, and package-content and isolated-import checks at 7/7 each. Both test suites also passed on a fresh clone. Use the &lt;a href="https://github.com/gamedevtoollab-blip/unity6-shadergraph-techniques/tree/846cb951577036ccbfb7ea24cf2568582f2ae1f5" rel="noopener noreferrer"&gt;pinned commit&lt;/a&gt; to follow along with the video and implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you explain the inputs and assumptions when the effect moves to a different mesh, position, or project?&lt;/strong&gt; Getting that right is what makes a Shader Graph easier to bring into your own game. Just do not confuse a recording at 60 fps with real-time performance—measure in your game's actual target environment.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/WhatsNewUnity63.html" rel="noopener noreferrer"&gt;New in Unity 6.3&lt;/a&gt;. Revisited September 14, 2026.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/universalrp-asset.html" rel="noopener noreferrer"&gt;Universal Render Pipeline asset reference for URP&lt;/a&gt;. Depth Texture, Opaque Texture, Opaque Downsampling, and related settings.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/prebuilt-shader-graphs-urp-lit.html" rel="noopener noreferrer"&gt;Lit shader graph reference for URP&lt;/a&gt;. Vertex/Fragment inputs and graph settings.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/prebuilt-shader-graphs-urp-unlit.html" rel="noopener noreferrer"&gt;Unlit shader graph reference for URP&lt;/a&gt;. Transparent blending, Render Face, Depth Write/Test, and related settings.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/post-processing-bloom.html" rel="noopener noreferrer"&gt;Bloom in URP&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/camera-component-reference.html" rel="noopener noreferrer"&gt;Camera component reference for URP&lt;/a&gt;. Camera Post Processing and Volume-related settings.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/integration-with-post-processing.html" rel="noopener noreferrer"&gt;Post-processing in URP&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Step-Node.html" rel="noopener noreferrer"&gt;Step Node / Shader Graph 17.3&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn9"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Smoothstep-Node.html" rel="noopener noreferrer"&gt;Smoothstep Node / Shader Graph 17.3&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn10"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Position-Node.html" rel="noopener noreferrer"&gt;Position Node / Shader Graph 17.3&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn11"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Transform-Node.html" rel="noopener noreferrer"&gt;Transform Node / Shader Graph 17.3&lt;/a&gt;. Coordinate spaces and the differences between Position, Direction, and Normal.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn12"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Property-Types.html" rel="noopener noreferrer"&gt;Property Types / Shader Graph 17.3&lt;/a&gt;. Reference, Show In Inspector, and related settings.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn13"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Time-Node.html" rel="noopener noreferrer"&gt;Time Node / Shader Graph 17.3&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn14"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Simple-Noise-Node.html" rel="noopener noreferrer"&gt;Simple Noise Node / Shader Graph 17.3&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn15"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Scene-Depth-Difference-Node.html" rel="noopener noreferrer"&gt;Scene Depth Difference node / Shader Graph 17.3&lt;/a&gt;. Eye mode, input coordinates, and the sign of the difference.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn16"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Fresnel-Effect-Node.html" rel="noopener noreferrer"&gt;Fresnel Effect Node / Shader Graph 17.3&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn17"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Screen-Position-Node.html" rel="noopener noreferrer"&gt;Screen Position Node / Shader Graph 17.3&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn18"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Scene-Color-Node.html" rel="noopener noreferrer"&gt;Scene Color node / Shader Graph 17.3&lt;/a&gt;. Opaque Texture, shader stage, and draw-order requirements.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn19"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/texture-type-default.html" rel="noopener noreferrer"&gt;Default texture type reference&lt;/a&gt;. sRGB, Mip Maps, and Wrap Mode.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn20"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Triplanar-Node.html" rel="noopener noreferrer"&gt;Triplanar Node / Shader Graph 17.3&lt;/a&gt;. Three-direction projection, three texture samples, and Input Space.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn21"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Normal-Vector-Node.html" rel="noopener noreferrer"&gt;Normal Vector Node / Shader Graph 17.3&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn22"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Renderer-localBounds.html" rel="noopener noreferrer"&gt;Renderer.localBounds / Unity 6.3&lt;/a&gt;. Bounds for shader deformation and persistence limitations.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn23"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/DrawCallBatching.html" rel="noopener noreferrer"&gt;Introduction to batching meshes&lt;/a&gt;. Static/Dynamic Batching and world space.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn24"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/AssetMetadata.html" rel="noopener noreferrer"&gt;Asset metadata&lt;/a&gt;. Asset identifiers, import settings, and preserving &lt;code&gt;.meta&lt;/code&gt; files.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn25"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/AssetPackagesCreate.html" rel="noopener noreferrer"&gt;Create asset packages&lt;/a&gt; and &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/AssetDatabase.ExportPackage.html" rel="noopener noreferrer"&gt;AssetDatabase.ExportPackage&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn26"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/AssetPackagesImport.html" rel="noopener noreferrer"&gt;Import local asset packages&lt;/a&gt;. Importing through &lt;code&gt;Assets &amp;gt; Import Package &amp;gt; Custom Package&lt;/code&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn27"&gt;
&lt;p&gt;Unity documentation: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/MaterialPropertyBlock.html" rel="noopener noreferrer"&gt;MaterialPropertyBlock / Unity 6.3&lt;/a&gt;. The SRP Batcher compatibility warning.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>unity3d</category>
      <category>shadergraph</category>
      <category>gamedev</category>
      <category>graphics</category>
    </item>
    <item>
      <title>12 Practical Unity 2D Shader Graph Effects (with a Sample Project)</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Tue, 15 Sep 2026 04:37:53 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/12-practical-unity-2d-shader-graph-effects-with-a-sample-project-14ej</link>
      <guid>https://dev.to/gamedevtoollab/12-practical-unity-2d-shader-graph-effects-with-a-sample-project-14ej</guid>
      <description>&lt;p&gt;You can transform the look of a Unity 2D game with shaders without redrawing the original artwork. Hit flashes, dissolves, color variants, glitches, holograms, the illusion of surface depth, and world-space scans all come down to manipulating UVs, color, alpha, and coordinates.&lt;/p&gt;

&lt;p&gt;The implementation has its traps: packing a sprite into an Atlas changes the density of a pattern, an outline samples a neighboring sprite, moving vertices does not produce a smooth bend, or 2D lighting suddenly becomes expensive. This article covers 12 techniques together with ways to avoid those problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch the effects in motion
&lt;/h2&gt;

&lt;p&gt;There is a demonstration video for the 2D shader effects in this article. Still images do not communicate timing and deformation particularly well, so start with the video to see the intended results, then jump to the implementation that interests you.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/_djMgAD57n4" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Use the video to inspect the appearance and motion. Use the article for coordinate choices, Sprite Atlas precautions, Shader Graph construction, and performance considerations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the sample project
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/gamedevtoollab-blip/unity-2d-shader-graph-effects/tree/c12a8ab0737b92029746795ec22f178c831ed3fd" rel="noopener noreferrer"&gt;GitHub sample project (inspected commit &lt;code&gt;c12a8ab&lt;/code&gt;)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The distributed project uses &lt;strong&gt;Unity 6000.5.11f1 and URP / Shader Graph 17.5.0&lt;/strong&gt;. Keep it separate from the build-it-yourself recipes below, which target Unity 6.4 / Shader Graph 17.4. Compatibility of the distributed graphs with older versions has not been verified.&lt;/p&gt;

&lt;h3&gt;
  
  
  File locations and running the demos
&lt;/h3&gt;

&lt;p&gt;Combine the filename in each technique with the corresponding folder below to get its full path. All paths are relative to the repository root.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Label in each technique&lt;/th&gt;
&lt;th&gt;Folder&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;Graph&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Assets/ShaderCapture/Shaders/Graphs/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Edit the shader nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Material&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Assets/ShaderCapture/Materials/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Duplicate and assign to a SpriteRenderer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scene&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Assets/Scenes/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inspect the complete setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Art&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Assets/ShaderCapture/Art/Generated/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Images and sprite geometry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Assets/ShaderCapture/Scripts/Runtime/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Control components&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Open the complete project through Unity Hub, then open &lt;code&gt;Assets/Scenes/00_CaptureHub.unity&lt;/code&gt; or the Scene listed for a technique and enter Play Mode. &lt;code&gt;Space&lt;/code&gt; plays or pauses, the left and right arrow keys adjust the effect amount, and &lt;code&gt;R&lt;/code&gt; resets the demo. The Scene files are already stored in the locations above; you do not need to run the generation menu commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared controls and requirements for reuse
&lt;/h3&gt;

&lt;p&gt;Start with &lt;code&gt;DemoCharacter.png&lt;/code&gt; in the Art folder. &lt;strong&gt;Copying a Material does not copy the demo controls.&lt;/strong&gt; In a different Scene, add &lt;code&gt;EffectDemoController.cs&lt;/code&gt; from Runtime to an empty GameObject. Assign the Art files &lt;code&gt;Noise.png&lt;/code&gt;, &lt;code&gt;DemoCharacter_Index.png&lt;/code&gt;, and &lt;code&gt;Palette.png&lt;/code&gt; to &lt;code&gt;Noise Texture&lt;/code&gt;, &lt;code&gt;Index Texture&lt;/code&gt;, and &lt;code&gt;Palette Texture&lt;/code&gt;, respectively. This controller supplies &lt;code&gt;_SC_Effect&lt;/code&gt;, &lt;code&gt;_SC_DemoTime&lt;/code&gt;, colors, textures, and other global shader values. It requires the Input System. Scene navigation uses the original build-index order. Avoid multiple controllers overwriting the same global values.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;per-renderer settings&lt;/strong&gt; mentioned in each technique are Inspector fields on &lt;code&gt;ShaderCapturePropertyBlock.cs&lt;/code&gt;, also in Runtime. Attach it to the target Renderer. It does not supply the shared global values. The recipe properties &lt;code&gt;_Effect&lt;/code&gt; and &lt;code&gt;_ScanCenter&lt;/code&gt; are different names from the sample properties &lt;code&gt;_SC_Effect&lt;/code&gt; and &lt;code&gt;_SC_ScanCenter&lt;/code&gt;; the controllers shown in the article cannot be used with the sample graphs unchanged.&lt;/p&gt;

&lt;p&gt;When moving an effect to another project, copy &lt;strong&gt;the Graph, Material, technique-specific dependencies, and shared controls&lt;/strong&gt;, including their &lt;code&gt;.meta&lt;/code&gt; files. Do not rely on Export Assets to discover textures assigned through global properties. When changing the artwork, update &lt;code&gt;_SC_SpriteSize&lt;/code&gt;, which is fixed at 2.56, and &lt;code&gt;_SC_TexelSize&lt;/code&gt;, which is derived from the Index Texture. The distributed graphs have &lt;code&gt;Use TexelSize&lt;/code&gt; disabled and use this separate value instead; do not overwrite their settings with the build-it-yourself Property table below. They do not automatically adapt to arbitrary pivots or Atlas layouts, so first check them outside an Atlas.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The sample-file guidance is based on source inspection at the pinned commit. Launching the project, compiling it in Unity, and testing an actual transfer to another project were not performed for this edition.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Target environment for the recipes
&lt;/h2&gt;

&lt;p&gt;Read the node, formula, and custom C# explanations below separately from the ready-made sample project above.&lt;/p&gt;

&lt;p&gt;The recipes target this configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unity 6.4&lt;/li&gt;
&lt;li&gt;Universal Render Pipeline (URP)&lt;/li&gt;
&lt;li&gt;2D Renderer&lt;/li&gt;
&lt;li&gt;Shader Graph 17.4&lt;/li&gt;
&lt;li&gt;SpriteRenderer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use a &lt;code&gt;Sprite Lit Shader Graph&lt;/code&gt; for effects that respond to 2D lights. The other effects generally use a &lt;code&gt;Sprite Unlit Shader Graph&lt;/code&gt;. The Built-in Render Pipeline has different targets and lighting behavior, so these recipes target URP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with three kinds of coordinates
&lt;/h2&gt;

&lt;p&gt;Many 2D shader problems begin with the choice of coordinates.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Coordinates&lt;/th&gt;
&lt;th&gt;Use cases&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Texture UV&lt;/td&gt;
&lt;td&gt;Original image, outlines, RGB offsets&lt;/td&gt;
&lt;td&gt;Refers to the packed region when using an Atlas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object Position&lt;/td&gt;
&lt;td&gt;Per-sprite dissolves, wind, local scan lines&lt;/td&gt;
&lt;td&gt;Independent of the Atlas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;World / Screen Position&lt;/td&gt;
&lt;td&gt;Waves crossing multiple objects, screen-fixed dithering&lt;/td&gt;
&lt;td&gt;The pattern does not stick to the object&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Sprite Atlas UVs are not necessarily local 0–1 coordinates
&lt;/h3&gt;

&lt;p&gt;A sprite packed into a Sprite Atlas has UVs that refer to positions in the entire Atlas. Feeding those UVs into &lt;code&gt;sin(UV.x * Frequency)&lt;/code&gt; or &lt;code&gt;floor(UV * Divisions)&lt;/code&gt; changes the density of stripes or waves when the sprite is packed.&lt;/p&gt;

&lt;p&gt;Separate the responsibilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sample the original image with the unmodified Texture UV.&lt;/li&gt;
&lt;li&gt;Generate patterns with 0–1 coordinates derived from Object Position.&lt;/li&gt;
&lt;li&gt;Specify Texture UV offsets in units such as “2 texels,” not an arbitrary value such as “0.01.”&lt;/li&gt;
&lt;li&gt;Increase Atlas Padding for effects with large UV offsets, or keep those sprites out of the Atlas.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Do not use one coordinate stream for both pattern generation and image sampling.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Atlas-independent local 0–1 coordinates
&lt;/h3&gt;

&lt;p&gt;For a SpriteRenderer using the Simple draw mode, derive local coordinates as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local01 = Position(Object).xy / SpriteSize + Pivot01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SpriteSize&lt;/code&gt; is the sprite's width and height in local units. &lt;code&gt;Pivot01&lt;/code&gt; is its pivot normalized within the Rect; a centered pivot is &lt;code&gt;(0.5, 0.5)&lt;/code&gt;. This component supplies those values:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;RequireComponent&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;SpriteRenderer&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;SpriteShaderDriver&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;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;SizeId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Shader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PropertyToID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"_SpriteSize"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;PivotId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Shader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PropertyToID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"_Pivot01"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;EffectId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Shader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PropertyToID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"_Effect"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;SeedId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Shader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PropertyToID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"_Seed"&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;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="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;effect&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="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;MaterialPropertyBlock&lt;/span&gt; &lt;span class="n"&gt;block&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;MaterialPropertyBlock&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;SpriteRenderer&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;Sprite&lt;/span&gt; &lt;span class="n"&gt;cachedSprite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;EnsureTarget&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;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="k"&gt;return&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;target&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;SpriteRenderer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&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="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;EnsureTarget&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="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;LateUpdate&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;EnsureTarget&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;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sprite&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;cachedSprite&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="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;SetEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clamp01&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&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;EnsureTarget&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;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetPropertyBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EffectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effect&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;SetPropertyBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Apply&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="nf"&gt;EnsureTarget&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;Sprite&lt;/span&gt; &lt;span class="n"&gt;sprite&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;sprite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;cachedSprite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sprite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Cache null too, avoiding reapplication every frame.&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;sprite&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="c1"&gt;// Rect-based 0-1 coordinates. Do not mix these with Tight Mesh bounds.&lt;/span&gt;
        &lt;span class="n"&gt;Vector2&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;sprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pixelsPerUnit&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;pivot&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;Vector2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;sprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pivot&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;sprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&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;sprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pivot&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;sprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&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;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetPropertyBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SizeId&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;Vector4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&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;size&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;0f&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;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PivotId&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;Vector4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pivot&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;pivot&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;0f&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;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EffectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SeedId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&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;SetPropertyBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&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 Shader Graph, Divide Object-space &lt;code&gt;Position.xy&lt;/code&gt; by &lt;code&gt;_SpriteSize&lt;/code&gt;, then Add &lt;code&gt;_Pivot01&lt;/code&gt;. &lt;code&gt;_SpriteSize&lt;/code&gt; is &lt;code&gt;sprite.rect.size / sprite.pixelsPerUnit&lt;/code&gt;. With either a centered or off-center pivot, the Rect's bottom-left maps to 0 and its top-right maps to 1. An asymmetric Tight Mesh occupies only part of that 0–1 range, but the definition of the Rect coordinates does not change.&lt;/p&gt;

&lt;p&gt;To normalize the visible Tight bounds instead, use a separate approach: supply both &lt;code&gt;bounds.min&lt;/code&gt; and &lt;code&gt;bounds.size&lt;/code&gt;, then calculate &lt;code&gt;(Position - BoundsMin) / BoundsSize&lt;/code&gt;. Do not mix that approach with the pivot-based formula. For Sliced, Tiled, Sprite Skin, or configurations that make frequent use of Flip, check that the Renderer's actual mesh matches the local coordinates you expect.&lt;/p&gt;

&lt;p&gt;MaterialPropertyBlock supplies per-renderer values, but removes SRP Batcher compatibility. It is convenient for a small number of individual settings. When setting values on many sprites every frame, compare it with shared Materials and global properties, and measure on the target device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a common base graph
&lt;/h2&gt;

&lt;p&gt;Before implementing the techniques, build the shared portion in a Sprite Unlit Shader Graph. These are the Properties for the Shader Graph 17.4 recipes:&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;Type&lt;/th&gt;
&lt;th&gt;Reference&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Supply / attribute&lt;/th&gt;
&lt;th&gt;Settings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Main Texture&lt;/td&gt;
&lt;td&gt;Texture 2D&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_MainTex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per Material&lt;/td&gt;
&lt;td&gt;Read Only ON (&lt;code&gt;PerRendererData&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Set as Main Texture ON / Use TexelSize ON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mask Texture&lt;/td&gt;
&lt;td&gt;Texture 2D&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_MaskTex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per Material&lt;/td&gt;
&lt;td&gt;Read Only ON (&lt;code&gt;PerRendererData&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Match the Secondary Texture name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Normal Map&lt;/td&gt;
&lt;td&gt;Texture 2D&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_NormalMap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per Material&lt;/td&gt;
&lt;td&gt;Read Only ON (&lt;code&gt;PerRendererData&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Mode: Normal Map / Fallback: Flat Normal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sprite Size&lt;/td&gt;
&lt;td&gt;Vector 2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_SpriteSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per Material&lt;/td&gt;
&lt;td&gt;Override per Renderer with an MPB&lt;/td&gt;
&lt;td&gt;Rect size in Object Space&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pivot 01&lt;/td&gt;
&lt;td&gt;Vector 2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_Pivot01&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per Material&lt;/td&gt;
&lt;td&gt;Override per Renderer with an MPB&lt;/td&gt;
&lt;td&gt;Pivot within the Rect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effect&lt;/td&gt;
&lt;td&gt;Float&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_Effect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per Material&lt;/td&gt;
&lt;td&gt;Override per Renderer with an MPB&lt;/td&gt;
&lt;td&gt;0–1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seed&lt;/td&gt;
&lt;td&gt;Float&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_Seed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per Material&lt;/td&gt;
&lt;td&gt;Override per Renderer with an MPB&lt;/td&gt;
&lt;td&gt;Per-object variation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three Texture Properties use &lt;code&gt;Per Material&lt;/code&gt;, an actual Scope option in the Shader Graph 17.4 UI. &lt;code&gt;Per Renderer Data&lt;/code&gt; is not a Scope option. Enabling &lt;code&gt;Read Only&lt;/code&gt; adds the &lt;code&gt;PerRendererData&lt;/code&gt; attribute so that SpriteRenderer can supply textures per renderer. The Normal Map fallback is Flat Normal. An Unlit Graph that does not use Secondary Textures does not need &lt;code&gt;_MaskTex&lt;/code&gt; or &lt;code&gt;_NormalMap&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a Texture 2D Property with Reference &lt;code&gt;_MainTex&lt;/code&gt; and mark it as the Main Texture.&lt;/li&gt;
&lt;li&gt;Read the original image with &lt;code&gt;Sample Texture 2D&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Connect RGB to the Fragment Base Color.&lt;/li&gt;
&lt;li&gt;Connect A to the Fragment Alpha.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;Alpha&lt;/code&gt; as the starting &lt;code&gt;Blending Mode&lt;/code&gt; in the Graph Inspector for an ordinary sprite.&lt;/li&gt;
&lt;li&gt;Leave &lt;code&gt;Disable Color Tint&lt;/code&gt; off when using &lt;code&gt;SpriteRenderer.color&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From here on, &lt;code&gt;baseRGBA&lt;/code&gt; means the original image's RGBA, &lt;code&gt;textureUV&lt;/code&gt; means the UV used to sample that image, and &lt;code&gt;local01&lt;/code&gt; means 0–1 coordinates derived from Object Position.&lt;/p&gt;

&lt;p&gt;Processing RGB in transparent regions before ordinary alpha blending can produce dark or light fringes. As a rule, multiply the final alpha by &lt;code&gt;baseRGBA.a&lt;/code&gt;. If the edges of a transparent PNG are dirty, also inspect &lt;code&gt;Alpha Is Transparency&lt;/code&gt; in the Import Settings, Sprite Atlas Padding, and the compression format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 1: Hit flashes and invincibility blinking
&lt;/h2&gt;

&lt;p&gt;▶ &lt;a href="https://www.youtube.com/watch?v=_djMgAD57n4&amp;amp;t=6s" rel="noopener noreferrer"&gt;Watch this effect (from 0:06)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample files&lt;/strong&gt; (use the folders listed at the beginning)&lt;br&gt;&lt;br&gt;
Graph: &lt;code&gt;Effect01_HitFlashInvincible.shadergraph&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Material: &lt;code&gt;Effect01.mat&lt;/code&gt; / Scene: &lt;code&gt;01_HitFlashInvincible.unity&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Use the per-renderer &lt;code&gt;Seed Offset&lt;/code&gt; setting to vary the blinking phase. The Bloom file is &lt;code&gt;Assets/ShaderCapture/Settings/ShaderCaptureVolumeProfile.asset&lt;/code&gt;. &lt;strong&gt;In the inspected version, its saved component reference is empty (&lt;code&gt;fileID: 0&lt;/code&gt;), so copying it alone does not reproduce Bloom.&lt;/strong&gt; Configure the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create and assign a new Profile to a Global Volume with Weight set to 1, then add &lt;code&gt;Add Override &amp;gt; Post-processing &amp;gt; Bloom&lt;/code&gt;. Match the generation code with Threshold = 0.75, Intensity = 0.65, and Scatter = 0.62. Enable Bloom and the override checkbox for each setting.&lt;/li&gt;
&lt;li&gt;On the Camera used for rendering, enable &lt;code&gt;Rendering &amp;gt; Post Processing&lt;/code&gt; and include the Volume GameObject's Layer in &lt;code&gt;Volume Mask&lt;/code&gt;. HDR shader colors alone do not create a glow outside the sprite's silhouette.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;See the &lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/urp/add-post-processing.html" rel="noopener noreferrer"&gt;URP post-processing setup guide&lt;/a&gt; for the configuration requirements.&lt;/p&gt;

&lt;p&gt;A simple Lerp between the original color and a flash color makes a hit much easier to read.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;outRGB = Lerp(baseRGBA.rgb, FlashColor.rgb, FlashAmount)
outAlpha = baseRGBA.a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;_FlashColor&lt;/code&gt;, &lt;code&gt;_FlashAmount&lt;/code&gt;, and optionally &lt;code&gt;_FlashPower&lt;/code&gt;. For an emissive look, add an HDR color:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;outRGB = baseRGBA.rgb
       + FlashColor.rgb * FlashAmount * FlashPower
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When using Bloom, reserve high brightness for meaningful moments, such as an attack telegraph.&lt;/p&gt;

&lt;p&gt;For invincibility blinking, generate a square wave from Time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blink = Step(0.5, Fraction(Time * BlinkSpeed + Seed))
outAlpha = baseRGBA.a * Lerp(1, blink, Invincible)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change &lt;code&gt;Seed&lt;/code&gt; per Renderer to avoid perfectly synchronized blinking. Combine the blinking with color or rim effects to communicate the state even during the invisible part of the cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 2: Palette swapping for character color variants
&lt;/h2&gt;

&lt;p&gt;▶ &lt;a href="https://www.youtube.com/watch?v=_djMgAD57n4&amp;amp;t=13s" rel="noopener noreferrer"&gt;Watch this effect (from 0:13)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample files&lt;/strong&gt; (use the folders listed at the beginning)&lt;br&gt;&lt;br&gt;
Graph: &lt;code&gt;Effect02_PaletteSwap.shadergraph&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Material: &lt;code&gt;Effect02.mat&lt;/code&gt; / Scene: &lt;code&gt;02_PaletteSwap.unity&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This sample needs &lt;code&gt;DemoCharacter_Index.png&lt;/code&gt; and &lt;code&gt;Palette.png&lt;/code&gt; from Art. Its palette has four columns and four rows: the Index Map's R channel stores the color ID, G stores shading, and &lt;code&gt;_SC_PaletteRow&lt;/code&gt; selects row 0–3. Different artwork needs a corresponding Index Map.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Hue&lt;/code&gt; node can rotate the hue, but use an &lt;strong&gt;Index Map&lt;/strong&gt; and a &lt;strong&gt;Palette Texture&lt;/strong&gt; when skin, clothing, metal, and eyes need independent color changes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Main Texture: the visible sprite.&lt;/li&gt;
&lt;li&gt;Index Map: the color-group number for each pixel.&lt;/li&gt;
&lt;li&gt;Palette Texture: a small texture containing replacement colors in a horizontal row.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;encoded = Sample(IndexMap, textureUV).r
id = round(encoded * (PaletteCount - 1))
paletteUV = float2((id + 0.5) / PaletteCount, 0.5)
paletteRGB = Sample(PaletteTexture, paletteUV).rgb

outRGB = paletteRGB
outAlpha = baseRGBA.a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;To preserve shading, store brightness in the Index Map's G channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;outRGB = paletteRGB * Lerp(Shadow, Highlight, indexMap.g)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normalize the region IDs &lt;code&gt;0–PaletteCount - 1&lt;/code&gt; into the R channel's 0–1 range. G can hold shading and B an emission mask. Disable sRGB for the Index Map, use Point filtering, and disable compression and mipmaps when needed. Set the Palette Texture to Point filtering and Clamp wrapping as well.&lt;/p&gt;

&lt;p&gt;When the Main Texture is packed into an Atlas, the Index Map must be sampled with a matching layout. Associate it as a Secondary Texture, or supply a separate UV Rect for the Index Map. Sampling a non-atlased Index Map directly with Atlas UVs does not work.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Replace Color&lt;/code&gt; is sufficient for a one-off effect, but Index Maps are easier to manage for a large set of assets containing anti-aliasing and shading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 3: Dissolves for burning, freezing, and digitizing
&lt;/h2&gt;

&lt;p&gt;▶ &lt;a href="https://www.youtube.com/watch?v=_djMgAD57n4&amp;amp;t=20s" rel="noopener noreferrer"&gt;Watch this effect (from 0:20)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample files&lt;/strong&gt; (use the folders listed at the beginning)&lt;br&gt;&lt;br&gt;
Graph: &lt;code&gt;Effect03_Dissolve.shadergraph&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Material: &lt;code&gt;Effect03.mat&lt;/code&gt; / Scene: &lt;code&gt;03_Dissolve.unity&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The sample uses &lt;code&gt;Noise.png&lt;/code&gt; from Art. The per-renderer &lt;code&gt;Dissolve Softness&lt;/code&gt; setting controls the boundary. Set &lt;code&gt;Dissolve Invert&lt;/code&gt; to 0 for disappearing or 1 for appearing. All four comparison views use the same Graph.&lt;/p&gt;

&lt;p&gt;A dissolve compares a noise value with the effect's progress and removes alpha accordingly. Split it into three regions—&lt;strong&gt;Body, Edge, and Hidden&lt;/strong&gt;—to make the result easier to control.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;threshold = Lerp(-EdgeWidth, 1 + EdgeWidth, Dissolve)
visible = Step(threshold, noise)
body = Step(threshold + EdgeWidth, noise)
edge = Max(visible - body, 0)

// At 0: fully visible, no Edge. At 1: fully hidden.
start = 1 - Step(0.000001, Dissolve)
finish = Step(0.999999, Dissolve)
visible = Lerp(visible, 1, start) * (1 - finish)
body = Lerp(body, 1, start) * (1 - finish)
edge = edge * (1 - start) * (1 - finish)

outRGB = baseRGBA.rgb * body + EdgeColor.rgb * edge
outAlpha = baseRGBA.a * visible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At &lt;code&gt;Dissolve=0&lt;/code&gt;, noise values 0, 0.5, and 1 all produce Body. At &lt;code&gt;Dissolve=1&lt;/code&gt;, they all produce Hidden. At 0.5, low noise values are Hidden, high values are Body, and Edge appears only along the boundary. &lt;code&gt;Max&lt;/code&gt; keeps Edge nonnegative.&lt;/p&gt;

&lt;p&gt;For a Smoothstep version, keep the same threshold direction: use &lt;code&gt;visibleSoft=Smoothstep(threshold-Softness, threshold+Softness, noise)&lt;/code&gt;, &lt;code&gt;bodySoft=Smoothstep(threshold+EdgeWidth-Softness, threshold+EdgeWidth+Softness, noise)&lt;/code&gt;, and &lt;code&gt;edgeSoft=Saturate(visibleSoft-bodySoft)&lt;/code&gt;. Apply the same start/finish guards. Invert with &lt;code&gt;One Minus&lt;/code&gt; to use the effect for appearing instead.&lt;/p&gt;

&lt;p&gt;Noise and coordinate choices change the impression:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Look&lt;/th&gt;
&lt;th&gt;Example input&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fire&lt;/td&gt;
&lt;td&gt;Broad Gradient Noise + an orange HDR Edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ice&lt;/td&gt;
&lt;td&gt;Voronoi + a thin light-blue Edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Digitization&lt;/td&gt;
&lt;td&gt;Noise quantized into horizontal bands + cyan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turning to ash&lt;/td&gt;
&lt;td&gt;Add World Position.y to Noise to progress from bottom to top&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reversing petrification&lt;/td&gt;
&lt;td&gt;Use region masks to offset the start time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Object Position makes the effect progress through the same relative portion of each sprite. World Position makes it cross multiple objects at the same height.&lt;/p&gt;

&lt;p&gt;For many sprites or a mobile target, measure whether a small shared Noise Texture performs better than procedural noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 4: Outer outlines and inner rims
&lt;/h2&gt;

&lt;p&gt;▶ &lt;a href="https://www.youtube.com/watch?v=_djMgAD57n4&amp;amp;t=27s" rel="noopener noreferrer"&gt;Watch this effect (from 0:27)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample files&lt;/strong&gt; (use the folders listed at the beginning)&lt;br&gt;&lt;br&gt;
Graph: &lt;code&gt;Effect04_OutlineInnerRim.shadergraph&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Material: &lt;code&gt;Effect04.mat&lt;/code&gt; / Scene: &lt;code&gt;04_OutlineInnerRim.unity&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Set the per-renderer &lt;code&gt;Outline Directions&lt;/code&gt; to 4 or 8. Compare the geometry using &lt;code&gt;DemoCharacter.png&lt;/code&gt; and &lt;code&gt;DemoCharacter_Tight.png&lt;/code&gt; from Art. &lt;code&gt;ShaderCapture.spriteatlas&lt;/code&gt; has Rotation OFF, Tight Packing OFF, and Padding 8, but you must still check the transparent margin and maximum UV offset.&lt;/p&gt;

&lt;p&gt;Create an outline by comparing the center pixel's alpha with the alpha of neighboring pixels.&lt;/p&gt;
&lt;h3&gt;
  
  
  Four-direction outline
&lt;/h3&gt;

&lt;p&gt;Let &lt;code&gt;texel&lt;/code&gt; be the UV size of one texel in the source texture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a0 = Alpha(textureUV)
aL = Alpha(textureUV + (-texel.x, 0))
aR = Alpha(textureUV + ( texel.x, 0))
aU = Alpha(textureUV + (0,  texel.y))
aD = Alpha(textureUV + (0, -texel.y))

neighbor = Max(aL, aR, aU, aD)
outline = Saturate(neighbor - a0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Composite the output as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;outRGB = Lerp(OutlineColor.rgb, baseRGBA.rgb, a0)
outAlpha = Max(a0, outline * OutlineColor.a)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An eight-direction version adds diagonal samples and looks smoother, but takes nine samples including the center. It may be suitable for a few selected objects, but not as a permanent effect on hundreds of grass sprites or tiles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Changing the thickness
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;texel * OutlinePixels&lt;/code&gt; as the offset. As the thickness increases to 2, 3, or 4 texels, the samples can skip intermediate pixels and leave gaps. For thick outlines, add samples at different distances or prepare a Signed Distance Field in advance to keep quality and cost manageable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inner rim
&lt;/h3&gt;

&lt;p&gt;To illuminate only the inside edge rather than adding color outside the sprite, use the minimum neighboring alpha:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inner = a0 * (1 - Min(aL, aR, aU, aD))
outRGB = baseRGBA.rgb + InnerColor.rgb * inner * Strength
outAlpha = a0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works well for state indicators: a red inner rim during a hit, or a light-blue rim while frozen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sprite Atlas and Mesh Type pitfalls
&lt;/h3&gt;

&lt;p&gt;Offsetting UVs outward can sample a neighboring sprite in the Atlas. Also, no fragments are generated outside the sprite's rendered mesh. If opaque pixels extend all the way to the image Rect's edges, the outer outline can be cut off.&lt;/p&gt;

&lt;p&gt;Check these conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disable &lt;code&gt;Allow Rotation&lt;/code&gt; for Atlases containing direction-dependent UV effects; this is a requirement for these recipes.&lt;/li&gt;
&lt;li&gt;For outer outlines, also check Atlas &lt;code&gt;Tight Packing&lt;/code&gt;. Disable it or use a separate Atlas when needed.&lt;/li&gt;
&lt;li&gt;Leave transparent space inside the source sprite's Rect at least as wide as the outline.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;Alpha Dilation&lt;/code&gt; for color bleeding at transparent edges; it does not enlarge the drawable region.&lt;/li&gt;
&lt;li&gt;Provide enough Sprite Atlas Padding.&lt;/li&gt;
&lt;li&gt;Use a separate Atlas for sprites with large effects extending outward.&lt;/li&gt;
&lt;li&gt;Consider &lt;code&gt;Full Rect&lt;/code&gt; if &lt;code&gt;Mesh Type: Tight&lt;/code&gt; cuts off the effect.&lt;/li&gt;
&lt;li&gt;Keep UV offsets within the range you planned for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sprite Atlases can pack sprites with a 90-degree rotation. With &lt;code&gt;Allow Rotation&lt;/code&gt; enabled, &lt;code&gt;textureUV.x/y&lt;/code&gt; may not match the sprite's local horizontal and vertical directions. These recipes assume &lt;code&gt;Allow Rotation&lt;/code&gt; is OFF for UV wobble, RGB glitches, pixelation, and directional outlines. To retain rotated packing, read packing information such as &lt;code&gt;Sprite.packingRotation&lt;/code&gt; on the CPU and supply a rotation basis to the shader. Full Rect alone cannot draw beyond the source Rect, so check transparent margins, Tight Packing, and Padding together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 5: Animating grass, cloth, and hair
&lt;/h2&gt;

&lt;p&gt;▶ &lt;a href="https://www.youtube.com/watch?v=_djMgAD57n4&amp;amp;t=34s" rel="noopener noreferrer"&gt;Watch this effect (from 0:34)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample files&lt;/strong&gt; (use the folders listed at the beginning)&lt;br&gt;&lt;br&gt;
Graph: &lt;code&gt;Effect05_WindVertexSquash.shadergraph&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Material: &lt;code&gt;Effect05.mat&lt;/code&gt; / Scene: &lt;code&gt;05_WindVertexSquash.unity&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;DemoCharacter_Quad.png&lt;/code&gt; (4 vertices) and &lt;code&gt;DemoCharacter_Subdivided.png&lt;/code&gt; (169 vertices) from Art &lt;strong&gt;together with their &lt;code&gt;.meta&lt;/code&gt; files&lt;/strong&gt;. Toggle the per-renderer &lt;code&gt;UV Wiggle Amount&lt;/code&gt;, &lt;code&gt;Vertex Wind Amount&lt;/code&gt;, and &lt;code&gt;Squash Amount&lt;/code&gt; settings between 0 and 1. This demo uses the sprite geometry stored with the PNG and &lt;code&gt;.meta&lt;/code&gt;, not a Mesh asset.&lt;/p&gt;

&lt;p&gt;There are two approaches to swaying: offset the UVs or offset the vertices. They can look similar, but differ in whether the silhouette changes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Wobbling only the UVs
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;phase = local01.y * Frequency
      + Time * Speed
      + Seed

offsetX = sin(phase) * AmplitudePixels
textureUV.x += offsetX * texel.x * local01.y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Multiplying by &lt;code&gt;local01.y&lt;/code&gt; at the end anchors the bottom and increases the movement toward the top. This works for grass, flames, patterns inside a flag, and water reflections.&lt;/p&gt;

&lt;p&gt;Specifying amplitude in pixel units such as “2 texels,” rather than “0.01 UV units,” makes adjustment easier when the Atlas size or source texture resolution changes.&lt;/p&gt;

&lt;p&gt;UV wobble alone does not change the sprite's rectangular geometry. Without enough transparent margin, the displaced image is clipped at the Rect's edges.&lt;/p&gt;
&lt;h3&gt;
  
  
  Moving the vertices
&lt;/h3&gt;

&lt;p&gt;Use Object Position as the starting point for the Master Stack's Vertex Position, and add a wave to X:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;positionOS.x += sin(
    local01.y * Frequency
    + Time * Speed
    + Seed) * Amplitude * local01.y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also moves the silhouette. However, a Full Rect sprite with only four vertices produces something closer to a shear than a smooth S-shaped curve.&lt;/p&gt;

&lt;p&gt;For smooth bending, use one of these approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give the sprite enough vertices through 2D Animation's Sprite Skin workflow.&lt;/li&gt;
&lt;li&gt;Subdivide the mesh in the Sprite Editor.&lt;/li&gt;
&lt;li&gt;Render a custom mesh with a MeshRenderer.&lt;/li&gt;
&lt;li&gt;Split the object into parts and animate them with Transforms or bones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A shader cannot create vertices that do not exist. If the graph seems correct but the sprite only tilts like a board, mesh density—not the calculation—is the issue.&lt;/p&gt;

&lt;p&gt;For a custom Graph using Sprite Skin, first check three prerequisites: (1) 2D Animation's SpriteSkin, (2) GPU Skinning enabled in Player Settings, and (3) SRP Batcher enabled in the URP Render Pipeline Asset. Feed Object-space Position, Normal, and Tangent into the &lt;code&gt;Sprite Skinning&lt;/code&gt; node and use its skinned Position, Normal, and Tangent outputs. Add custom offsets &lt;strong&gt;after&lt;/strong&gt; skinning, then connect the result to Vertex Position. Enabling SRP Batcher here is a functional prerequisite; losing SRP Batcher compatibility through MaterialPropertyBlock is a separate performance consideration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jelly-like squash
&lt;/h3&gt;

&lt;p&gt;For a landing squash, widen the vertices along X and compress them along Y:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;anchor01 = float2(0.5, 0.0) // Bottom anchor. For a center anchor, use float2(0.5, 0.5).
scale = float2(1 + Squash, 1 - Squash)
deformed01 = (local01 - anchor01) * scale + anchor01

// Convert normalized coordinates back to Object Space for Vertex Position.
deformedOS.xy = (deformed01 - Pivot01) * SpriteSize
deformedOS.z = Position(Object).z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At &lt;code&gt;Squash=0&lt;/code&gt;, &lt;code&gt;deformed01 == local01&lt;/code&gt;, so the result matches the original Object Position. Restoring Object Space with &lt;code&gt;Pivot01&lt;/code&gt; prevents a positional jump even with an off-center pivot. A bottom anchor uses &lt;code&gt;anchor01.y=0&lt;/code&gt;, keeping vertices at &lt;code&gt;local01.y=0&lt;/code&gt; fixed vertically. For a center anchor, use &lt;code&gt;anchor01=(0.5, 0.5)&lt;/code&gt;. Compared with animating Transform Scale through an Animator, shader deformation makes it easier to mask the deformation to specific regions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 6: Row-based glitches and RGB splitting
&lt;/h2&gt;

&lt;p&gt;▶ &lt;a href="https://www.youtube.com/watch?v=_djMgAD57n4&amp;amp;t=41s" rel="noopener noreferrer"&gt;Watch this effect (from 0:41)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample files&lt;/strong&gt; (use the folders listed at the beginning)&lt;br&gt;&lt;br&gt;
Graph: &lt;code&gt;Effect06_GlitchRgbSplit.shadergraph&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Material: &lt;code&gt;Effect06.mat&lt;/code&gt; / Scene: &lt;code&gt;06_GlitchRgbSplit.unity&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Additional required dependency: &lt;code&gt;Assets/ShaderCapture/Shaders/Includes/ShaderCaptureEffects.hlsl&lt;/code&gt; and its &lt;code&gt;.meta&lt;/code&gt;. The sample function is &lt;code&gt;Effect06Fragment&lt;/code&gt;, which also composites RGB and supports both float and half. It differs from the recipe's &lt;code&gt;GlitchUV&lt;/code&gt;, which only returns modified UVs. The sample HLSL contains four explicit texture samples: the original image, center, R, and B. Evaluate that separately from the three-sample minimal recipe below.&lt;/p&gt;

&lt;p&gt;A glitch usually feels more convincing when a few horizontal blocks suddenly shift rather than continuously wobble. Put the row-and-time-based random calculation in a Custom Function to keep the graph readable.&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="c1"&gt;// Assets/Shaders/Glitch2D.hlsl&lt;/span&gt;
&lt;span class="cp"&gt;#ifndef GLITCH_2D_INCLUDED
#define GLITCH_2D_INCLUDED
&lt;/span&gt;
&lt;span class="n"&gt;float&lt;/span&gt; &lt;span class="nf"&gt;Hash21&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;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;frac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="kt"&gt;float2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;456&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&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;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;32&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;frac&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="o"&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="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;GlitchUV_float&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="n"&gt;float&lt;/span&gt; &lt;span class="n"&gt;LocalY&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;TimeValue&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;BlockCount&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;Rate&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;AmountInTexels&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;TexelX&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;Threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;float2&lt;/span&gt; &lt;span class="n"&gt;OutUV&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;float&lt;/span&gt; &lt;span class="n"&gt;Mask&lt;/span&gt;&lt;span class="p"&gt;)&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;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LocalY&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="n"&gt;BlockCount&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="mi"&gt;0&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;tick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeValue&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="n"&gt;Rate&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="mo"&gt;001&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;random&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Hash21&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="n"&gt;Mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;OutUV&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="kt"&gt;float2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&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="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="n"&gt;AmountInTexels&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;TexelX&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Mask&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;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;GlitchUV_half&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;half2&lt;/span&gt; &lt;span class="n"&gt;UV&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;half&lt;/span&gt; &lt;span class="n"&gt;LocalY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;half&lt;/span&gt; &lt;span class="n"&gt;TimeValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;half&lt;/span&gt; &lt;span class="n"&gt;BlockCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;half&lt;/span&gt; &lt;span class="n"&gt;Rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;half&lt;/span&gt; &lt;span class="n"&gt;AmountInTexels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;half&lt;/span&gt; &lt;span class="n"&gt;TexelX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;half&lt;/span&gt; &lt;span class="n"&gt;Threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;half2&lt;/span&gt; &lt;span class="n"&gt;OutUV&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;half&lt;/span&gt; &lt;span class="n"&gt;Mask&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;float&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;GlitchUV_float&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;float2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;UV&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LocalY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BlockCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AmountInTexels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TexelX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;OutUV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;half2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;Mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;half&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;mask&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;Set the Custom Function Node to File Mode and enter &lt;code&gt;GlitchUV&lt;/code&gt; as the function name, without a suffix. The HLSL provides both &lt;code&gt;_float&lt;/code&gt; and &lt;code&gt;_half&lt;/code&gt;, so it resolves with Single or Half precision. Define the ports in this order: inputs &lt;code&gt;UV Vector2&lt;/code&gt;, &lt;code&gt;LocalY Float&lt;/code&gt;, &lt;code&gt;TimeValue Float&lt;/code&gt;, &lt;code&gt;BlockCount Float&lt;/code&gt;, &lt;code&gt;Rate Float&lt;/code&gt;, &lt;code&gt;AmountInTexels Float&lt;/code&gt;, &lt;code&gt;TexelX Float&lt;/code&gt;, and &lt;code&gt;Threshold Float&lt;/code&gt;; outputs &lt;code&gt;OutUV Vector2&lt;/code&gt; and &lt;code&gt;Mask Float&lt;/code&gt;. Match the names, types, and order to the HLSL arguments. Connect &lt;code&gt;local01.y&lt;/code&gt; to &lt;code&gt;LocalY&lt;/code&gt; and the original Texture UV to &lt;code&gt;UV&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;RGB splitting uses three samples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;right = Sample(MainTex, glitchUV + (chroma, 0))
center = Sample(MainTex, glitchUV)
left = Sample(MainTex, glitchUV - (chroma, 0))
r = right.r
g = center.g
b = left.b
a = center.a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is one sample on each side and one at the center: three &lt;code&gt;Sample Texture 2D&lt;/code&gt; nodes in total. G and alpha reuse the same center sample. If all three samples remain active during normal rendering, another option is to switch Materials only while the effect is playing.&lt;/p&gt;

&lt;p&gt;With an Atlas, provide Padding wider than the maximum &lt;code&gt;AmountInTexels&lt;/code&gt; so that UV offsets do not reach neighboring sprites. An effect that tears the entire screen is better suited to a Renderer Feature or full-screen Blit than an individual sprite shader.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 7: Pixelation, posterization, and dithering
&lt;/h2&gt;

&lt;p&gt;▶ &lt;a href="https://www.youtube.com/watch?v=_djMgAD57n4&amp;amp;t=48s" rel="noopener noreferrer"&gt;Watch this effect (from 0:48)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample files&lt;/strong&gt; (use the folders listed at the beginning)&lt;br&gt;&lt;br&gt;
Graph: &lt;code&gt;Effect07_PixelPosterizeDither.shadergraph&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Material: &lt;code&gt;Effect07.mat&lt;/code&gt; / Scene: &lt;code&gt;07_PixelPosterizeDither.unity&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Set the per-renderer &lt;code&gt;Dither Space&lt;/code&gt; to 0 for UV-based pixels or 1 for screen pixels. The sample adds a dither pattern to the color; it does not use the Alpha Clipping variation described below.&lt;/p&gt;

&lt;p&gt;For a pixel-art-like result, separate the process into three stages: UV quantization, color quantization, and dithering.&lt;/p&gt;
&lt;h3&gt;
  
  
  Pixelation
&lt;/h3&gt;

&lt;p&gt;Convert UVs to actual texture pixel coordinates, then quantize them into blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pixel = textureUV * TextureSize
pixelated = floor(pixel / BlockPixels) * BlockPixels
sampleUV = (pixelated + BlockPixels * 0.5) / TextureSize
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 0.5 samples the center of each block. In Shader Graph, build this with &lt;code&gt;Texel Size&lt;/code&gt;, Multiply, Divide, and Floor.&lt;/p&gt;

&lt;p&gt;Quantizing in pixel coordinates across the entire Atlas can look acceptable with small &lt;code&gt;BlockPixels&lt;/code&gt; values, but can sample neighboring sprites at the boundaries. To keep the effect strictly within each sprite, supply its UV Rect and convert to local coordinates, or keep the target sprite outside the Atlas.&lt;/p&gt;

&lt;h3&gt;
  
  
  Posterization
&lt;/h3&gt;

&lt;p&gt;Round RGB values to the desired number of levels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;quantized = round(color * (Levels - 1)) / (Levels - 1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shader Graph also has a &lt;code&gt;Posterize&lt;/code&gt; node. Around 4–8 levels produces retro-looking gradients. To constrain the actual colors rather than only their levels, map them to the nearest color in the Palette Texture from Technique 2.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dithering
&lt;/h3&gt;

&lt;p&gt;Add a &lt;code&gt;Dither&lt;/code&gt; node when reducing the number of colors produces banding.&lt;/p&gt;

&lt;p&gt;You can also enable Alpha Clipping in the Graph Inspector and connect the Dither result to Alpha Clip Threshold to represent transparency as a pattern of discarded pixels. However, &lt;strong&gt;Alpha Clipping and Depth Write are separate settings&lt;/strong&gt;. Transparent rendering with &lt;code&gt;Depth Write: Auto&lt;/code&gt; does not acquire opaque-cutout-style depth writing merely because Alpha Clipping is enabled. Consider &lt;code&gt;Force Enabled&lt;/code&gt; only for a specific need to write depth, and check its effects on 2D sorting, SpriteMask, and overlapping transparent objects. Prioritize sorting for ordinary 2D sprites.&lt;/p&gt;

&lt;p&gt;Using Screen Position for dithering keeps the pattern fixed on the screen while the sprite moves. Using UVs or &lt;code&gt;local01&lt;/code&gt; attaches it to the sprite. When combining this with a Pixel Perfect Camera, consistently use either the game resolution or the final output resolution as the pattern's reference; mixing them can make the pattern shimmer during camera movement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 8: Holograms, scan lines, and diagonal shine
&lt;/h2&gt;

&lt;p&gt;▶ &lt;a href="https://www.youtube.com/watch?v=_djMgAD57n4&amp;amp;t=55s" rel="noopener noreferrer"&gt;Watch this effect (from 0:55)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample files&lt;/strong&gt; (use the folders listed at the beginning)&lt;br&gt;&lt;br&gt;
Graph: &lt;code&gt;Effect08_HologramShine.shadergraph&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Material: &lt;code&gt;Effect08.mat&lt;/code&gt; / Scene: &lt;code&gt;08_HologramShine.unity&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This Graph combines scan lines, UV offsets, and diagonal shine using standard nodes. It does not need the HLSL from Technique 6. The horizontal-line and diagonal-shine branches can be adjusted separately.&lt;/p&gt;
&lt;h3&gt;
  
  
  Scan lines
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;line = sin((local01.y * LineCount + Time * ScrollSpeed) * 6.283185)
lineMask = Smoothstep(LineThreshold, 1, line)

outRGB = baseRGBA.rgb * HologramTint
       + LineColor * lineMask * LinePower

outAlpha = baseRGBA.a * Lerp(MinAlpha, 1, lineMask)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Use Screen Position.y to keep the lines fixed on the screen like a CRT, or &lt;code&gt;local01.y&lt;/code&gt; to attach them to a projected image. A mostly stable hologram with occasional horizontal shifts from Technique 6 is usually easier to read than a constantly unstable one.&lt;/p&gt;
&lt;h3&gt;
  
  
  Diagonal shine
&lt;/h3&gt;

&lt;p&gt;A dot product creates a band that sweeps across a card or item:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;p = dot(local01, float2(0.5, 0.5))
center = frac(Time * Speed)
d = abs(p - center)
d = min(d, 1 - d)
shine = 1 - Smoothstep(Width, Width + Softness, d)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;shine&lt;/code&gt; as an HDR color and multiply it by &lt;code&gt;baseRGBA.a&lt;/code&gt; to keep it out of transparent regions. A Mask Map that marks only metal or glass prevents faces and cloth from shining unnaturally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 9: Adding depth with normal maps and 2D lights
&lt;/h2&gt;

&lt;p&gt;▶ &lt;a href="https://www.youtube.com/watch?v=_djMgAD57n4&amp;amp;t=62s" rel="noopener noreferrer"&gt;Watch this effect (from 1:02)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample files&lt;/strong&gt; (use the folders listed at the beginning)&lt;br&gt;&lt;br&gt;
Graph: &lt;code&gt;Effect09_NormalMap2DLight.shadergraph&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Material: &lt;code&gt;Effect09.mat&lt;/code&gt; / Scene: &lt;code&gt;09_NormalMap2DLight.unity&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is a Sprite Lit Graph. Register &lt;code&gt;DemoCharacter_Normal.png&lt;/code&gt; from Art as the &lt;code&gt;_NormalMap&lt;/code&gt; Secondary Texture. To reproduce the moving Light 2D as well, include &lt;code&gt;DeterministicLightRig.cs&lt;/code&gt; from Runtime and preserve the Scene's reference assignments. In addition to the standard normal connection, the sample Graph emphasizes shading in Base Color.&lt;/p&gt;

&lt;p&gt;A Sprite Lit Shader Graph with a Normal Map registered as a Secondary Texture can add directional shading to a flat sprite.&lt;/p&gt;
&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Prepare a Normal Map with the same layout and dimensions as the Main Texture. If using a Mask Map, prepare one with the same layout and dimensions as well.&lt;/li&gt;
&lt;li&gt;Import the Normal Map with &lt;code&gt;Texture Type: Normal Map&lt;/code&gt; and sRGB OFF. Import the Mask Map with &lt;code&gt;Texture Type: Default&lt;/code&gt; and sRGB OFF. For pixel-precise masks, check for channel contamination from mipmaps and compression in the actual target texture format.&lt;/li&gt;
&lt;li&gt;Open Secondary Textures in the Sprite Editor, add the Normal Map, and set its Name to &lt;code&gt;_NormalMap&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If using a Mask Map, add it on the same Secondary Textures screen and set its Name to &lt;code&gt;_MaskTex&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Check the Texture assignments for &lt;code&gt;_NormalMap&lt;/code&gt; and, when used, &lt;code&gt;_MaskTex&lt;/code&gt;, then click &lt;code&gt;Apply&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create a Sprite Lit Shader Graph.&lt;/li&gt;
&lt;li&gt;Connect &lt;code&gt;_MainTex&lt;/code&gt;, &lt;code&gt;_MaskTex&lt;/code&gt;, and &lt;code&gt;_NormalMap&lt;/code&gt; to their respective &lt;code&gt;Sample Texture 2D&lt;/code&gt; nodes.&lt;/li&gt;
&lt;li&gt;Change the normal sample's Type to Normal.&lt;/li&gt;
&lt;li&gt;Connect the results to Base Color, Sprite Mask, Normal (Tangent Space), and Alpha in the Master Stack.&lt;/li&gt;
&lt;li&gt;Add Light 2D objects to the Scene and enable Normal Map support on the lights that need it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Automatically generated normal maps tend to turn line art into surface bumps. Refine the large forms first: the face, clumps of hair, and curved armor surfaces.&lt;/p&gt;
&lt;h3&gt;
  
  
  Performance considerations
&lt;/h3&gt;

&lt;p&gt;2D lighting uses Light Render Textures for each Blend Style. When normal maps are enabled, a full-size Render Texture is created for the depth prepass for each Layer Batch. The Unity 6.4 documentation describes this as expensive.&lt;/p&gt;

&lt;p&gt;Start with one Blend Style, enable normals only for the Sorting Layers that need them, and measure Light Render Texture Scale, light count, and shadows on the target device.&lt;/p&gt;
&lt;h2&gt;
  
  
  Technique 10: Masking wetness, metal highlights, and weak points
&lt;/h2&gt;

&lt;p&gt;▶ &lt;a href="https://www.youtube.com/watch?v=_djMgAD57n4&amp;amp;t=69s" rel="noopener noreferrer"&gt;Watch this effect (from 1:09)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample files&lt;/strong&gt; (use the folders listed at the beginning)&lt;br&gt;&lt;br&gt;
Graph: &lt;code&gt;Effect10_MaskMapLighting.shadergraph&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Material: &lt;code&gt;Effect10.mat&lt;/code&gt; / Scene: &lt;code&gt;10_MaskMapLighting.unity&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is a Sprite Lit Graph. Register &lt;code&gt;DemoCharacter_Mask.png&lt;/code&gt; from Art as the &lt;code&gt;_MaskTex&lt;/code&gt; Secondary Texture. The sample uses R for wetness, G for metal lighting, and B for a pulsing weak point. Set the per-renderer &lt;code&gt;Mask Channel&lt;/code&gt; to 0, 1, or 2; do not leave it at the initial value of −1. For G, the Light 2D uses Blend Style 3 (zero-based: Additive with Mask, G) from &lt;code&gt;Assets/Settings/Renderer2D.asset&lt;/code&gt;. Set the target sprite's Sorting Layer to &lt;code&gt;MaskMetal&lt;/code&gt; and configure the light to target that Sorting Layer. Recreate this correspondence in the destination project instead of overwriting its entire settings file.&lt;/p&gt;

&lt;p&gt;Assigning the channels of a Mask Map lets you distinguish materials within a single sprite.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;Example use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;Ordinary shading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td&gt;Metal or wet surfaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Magic or weak points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Reserved for another effect&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Set Mask Texture Channel in the 2D Renderer Data's Light Blend Styles, then select the corresponding Blend Style on the Light 2D. The Master Stack's &lt;code&gt;Sprite Mask&lt;/code&gt; is the input for this 2D lighting Mask Map; it is not the SpriteMask component.&lt;/p&gt;

&lt;p&gt;For rain, paint upward-facing surfaces and hair highlights into G, then multiply by &lt;code&gt;_Wetness&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;wet = mask.g * Wetness
outRGB += WetColor * wet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A boss core painted into B can pulse only while it is vulnerable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pulse = 0.5 + 0.5 * sin(Time * PulseSpeed)
weak = mask.b * WeakPoint * pulse
outRGB += WeakColor * weak * WeakPower
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pack color-variation, wetness, and weak-point data into RGBA without giving a channel conflicting meanings. Check for channel contamination caused by lossy compression in the actual device texture format. Adding color in a Lit Graph modifies Base Color before lighting. If you need emission that does not depend on lighting, use an additional Unlit sprite or a separate additive effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 11: Water reflections with a duplicated sprite
&lt;/h2&gt;

&lt;p&gt;▶ &lt;a href="https://www.youtube.com/watch?v=_djMgAD57n4&amp;amp;t=76s" rel="noopener noreferrer"&gt;Watch this effect (from 1:16)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample files&lt;/strong&gt; (use the folders listed at the beginning)&lt;br&gt;&lt;br&gt;
Graph: &lt;code&gt;Effect11_WaterReflection.shadergraph&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Material: &lt;code&gt;Effect11.mat&lt;/code&gt; / Scene: &lt;code&gt;11_WaterReflection.unity&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Assign the Material only to the duplicated reflection sprite. In the Scene, &lt;code&gt;Stable Subject&lt;/code&gt; is rendered normally, while &lt;code&gt;Reflection&lt;/code&gt; is flipped vertically, dimmed, and UV-distorted. The Material does not create the reflection object for you.&lt;/p&gt;

&lt;p&gt;For a small puddle or water in a side-scrolling scene, duplicating a SpriteRenderer is a lightweight alternative to using another Camera.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Place a reflection Renderer as a child of the original SpriteRenderer.&lt;/li&gt;
&lt;li&gt;Flip it vertically and position it below the waterline.&lt;/li&gt;
&lt;li&gt;Move it to the water's Sorting Layer.&lt;/li&gt;
&lt;li&gt;Apply a horizontal wave and an alpha fade.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wave = sin(local01.y * Frequency + Time * Speed)
     * AmplitudePixels

sampleUV.x = textureUV.x + wave * texel.x
fade = Smoothstep(0, FadeLength, 1 - local01.y)

outAlpha = baseRGBA.a * fade * ReflectionAlpha
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Depending on how the reflection is flipped, the waterline may be at the opposite end. Swap &lt;code&gt;local01.y&lt;/code&gt; and &lt;code&gt;1 - local01.y&lt;/code&gt; when needed. Keep distortion within the transparent margin and Atlas Padding. This method reflects only the duplicated sprite. For background objects and occlusion, use a Camera plus a Render Texture; for a full-screen effect, use a Renderer Feature.&lt;/p&gt;
&lt;h2&gt;
  
  
  Technique 12: World-space scans and circular reveals
&lt;/h2&gt;

&lt;p&gt;▶ &lt;a href="https://www.youtube.com/watch?v=_djMgAD57n4&amp;amp;t=83s" rel="noopener noreferrer"&gt;Watch this effect (from 1:23)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample files&lt;/strong&gt; (use the folders listed at the beginning)&lt;br&gt;&lt;br&gt;
Graph: &lt;code&gt;Effect12_WorldScanReveal.shadergraph&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Material: &lt;code&gt;Effect12.mat&lt;/code&gt; / Scene: &lt;code&gt;12_WorldScanReveal.unity&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Share the Material, &lt;code&gt;_SC_ScanCenter&lt;/code&gt;, and &lt;code&gt;_SC_ScanRadius&lt;/code&gt; across the target sprites. The sample's reveal does not completely hide the area outside the radius. To make it fully invisible, use the alpha formula below.&lt;/p&gt;

&lt;p&gt;World Position lets one wave sweep across multiple sprites. Uses include item searches, sonar, boss attack telegraphs, and the end of a time-stop effect.&lt;/p&gt;
&lt;h3&gt;
  
  
  Circular scan
&lt;/h3&gt;

&lt;p&gt;Supply a world-space center, &lt;code&gt;_ScanCenter&lt;/code&gt;, and a radius, &lt;code&gt;_ScanRadius&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;distance = Distance(Position(World).xy, ScanCenter.xy)

ring = 1 - Smoothstep(
    RingWidth,
    RingWidth + Softness,
    abs(distance - ScanRadius))

outRGB = baseRGBA.rgb
       + ScanColor.rgb * ring
       * baseRGBA.a * ScanPower
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Global shader properties are appropriate when all targets use the same values. Set the &lt;code&gt;Scope&lt;/code&gt; of &lt;code&gt;_ScanCenter&lt;/code&gt; and &lt;code&gt;_ScanRadius&lt;/code&gt; to &lt;code&gt;Global&lt;/code&gt; in the Blackboard. If they remain &lt;code&gt;Per Material&lt;/code&gt;, the Material values take precedence.&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;WorldScanController&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;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;CenterId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Shader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PropertyToID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"_ScanCenter"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;RadiusId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Shader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PropertyToID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"_ScanRadius"&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="n"&gt;Transform&lt;/span&gt; &lt;span class="n"&gt;center&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="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;radius&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;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;p&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;center&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;center&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;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;Shader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetGlobalVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CenterId&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;Vector4&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="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;Shader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetGlobalFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RadiusId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Turning the scan into a reveal
&lt;/h3&gt;

&lt;p&gt;To show only the area within the radius, multiply alpha by this mask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inside = 1 - Smoothstep(
    ScanRadius - Softness,
    ScanRadius,
    distance)

outAlpha = baseRGBA.a * inside
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For large numbers of hidden objects, disable distant Renderers and use the shader fade only near the boundary.&lt;/p&gt;

&lt;p&gt;World Position makes the sprite appear to pass through the light. Object Position attaches the pattern to the sprite. Screen Position fixes it to the screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combine techniques into more expressive effects
&lt;/h2&gt;

&lt;p&gt;A small set of combinations with clear gameplay meaning is easier to maintain than a large collection of unrelated effects.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Combination&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Taking damage&lt;/td&gt;
&lt;td&gt;Brief white Flash + red Inner Rim + a momentary Glitch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teleporting&lt;/td&gt;
&lt;td&gt;Dissolve + HDR Edge + RGB splitting just before disappearance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rare card&lt;/td&gt;
&lt;td&gt;Palette + diagonal Shine + glow restricted to the metal Mask&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reversing petrification&lt;/td&gt;
&lt;td&gt;Desaturation + Voronoi Dissolve + a return to the original Palette&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Drive several features from one &lt;code&gt;_Effect&lt;/code&gt; value with offset timing, and the script only needs to animate a single value from 0 to 1. For example, remap 0–0.2 for the Flash, 0.15–0.9 for the Dissolve, and 0.85–1.0 for the final glow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five factors that determine performance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Texture sample count
&lt;/h3&gt;

&lt;p&gt;These estimates describe the article's minimal implementations. Measure them separately from the distributed sample's extra processing and the complete comparison Scenes.&lt;/p&gt;

&lt;p&gt;Flash uses one sample; Palette uses three (Main + Index + Palette); a four-direction Outline uses five; an eight-direction Outline uses nine; and RGB Glitch uses three. Even at &lt;code&gt;_Effect = 0&lt;/code&gt;, the effect's texture samples can remain in a graph that evaluates both sides of a Lerp. One option is to use a lightweight shader for the normal state and a different shader only while the effect is active.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Transparent overdraw
&lt;/h3&gt;

&lt;p&gt;Large transparent margins, oversized particles, and nearly full-screen holograms can make overdraw more important than instruction count.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Render Textures for 2D lighting
&lt;/h3&gt;

&lt;p&gt;Each Blend Style needs a Light Render Texture. Unity's optimization guide gives examples of using one for a simple Scene and keeping more general cases to around two. Include normal maps, shadows, and the light sets for each Sorting Layer in your measurements.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Per-renderer values
&lt;/h3&gt;

&lt;p&gt;Accessing &lt;code&gt;renderer.material&lt;/code&gt; creates a Material instance. MaterialPropertyBlock avoids that duplication, but removes SRP Batcher compatibility.&lt;/p&gt;

&lt;p&gt;Choose the data path according to its scope:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shared everywhere: shader properties with Global Scope.&lt;/li&gt;
&lt;li&gt;Shared by a group: a shared Material.&lt;/li&gt;
&lt;li&gt;A few individual characters: MaterialPropertyBlock.&lt;/li&gt;
&lt;li&gt;Large amounts of per-object data: vertex streams, additional UVs, or custom meshes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Shader variants
&lt;/h3&gt;

&lt;p&gt;Keywords can remove unnecessary calculations by disabling features, but their combinations increase the number of variants. Do not combine Flash, Outline, Dissolve, and Glitch without limit in a single shader. Group only the features that need to run together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failures and their causes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Main cause and remedy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A horizontal offset becomes vertical after packing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Allow Rotation&lt;/code&gt; rotated the packed sprite. Disable it for direction-dependent UV effects.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An outer Outline is clipped or samples a neighbor&lt;/td&gt;
&lt;td&gt;Check Tight Packing, transparent margins, Alpha Dilation, Padding, and Mesh Type together.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripes become denser after packing into an Atlas&lt;/td&gt;
&lt;td&gt;The pattern uses Texture UVs. Separate it into &lt;code&gt;local01&lt;/code&gt; derived from Object Position.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UV wobble reveals a neighboring image&lt;/td&gt;
&lt;td&gt;Atlas Padding is insufficient. Revisit the maximum offset, Padding, and Atlas grouping.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An Outline is cut off at the rectangle's edge&lt;/td&gt;
&lt;td&gt;Nothing is drawn outside the Sprite Rect or mesh. Add transparent margin and use Full Rect when needed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vertex deformation only tilts the sprite like a board&lt;/td&gt;
&lt;td&gt;There are not enough vertices. Use Sprite Skin or a subdivided mesh.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transparent edges look black&lt;/td&gt;
&lt;td&gt;Check the RGB of transparent pixels, blending, Padding, compression, and whether the original alpha was applied.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SpriteRenderer.color is applied twice&lt;/td&gt;
&lt;td&gt;Reconcile &lt;code&gt;Disable Color Tint&lt;/code&gt; with any manual Vertex Color multiplication so tint is applied once.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The effect is cheap on PC but expensive on the target device&lt;/td&gt;
&lt;td&gt;Check resolution, overdraw, texture formats, and 2D lighting Render Textures on the device GPU.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which techniques should you introduce first?
&lt;/h2&gt;

&lt;p&gt;A cautious implementation order is Flash, Palette, Dissolve, Inner Rim, Shine, selective use of Outline, and finally Normal Map + 2D Light. The later techniques need more attention to Atlases, additional samples, and Light Render Textures.&lt;/p&gt;

&lt;p&gt;Give color and motion consistent meanings: white for damage, a red inner rim for danger, a blue scan for search targets, and diagonal shine for rare items. The visual effects then become part of the game's information design.&lt;/p&gt;

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

&lt;p&gt;Unity 2D shader effects can be broken down into operations on UVs, RGB, alpha, and Object, World, or Screen coordinates. Separate Texture UVs from pattern coordinates, respect Atlas boundaries and the original alpha, and measure sample count and overdraw on the target device.&lt;/p&gt;

&lt;p&gt;Start by adding Flash and Dissolve to a Sprite Unlit Shader Graph. Extract reusable parts into Sub Graphs, and the remaining effects can be built with the same ideas.&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/urp/ShaderGraph.html" rel="noopener noreferrer"&gt;Unity Manual: Create 2D lit shader with Shader Graph&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/Manual/urp/Lights-2D-intro.html" rel="noopener noreferrer"&gt;Unity Manual: 2D lighting system in URP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/Manual/urp/SecondaryTextures.html" rel="noopener noreferrer"&gt;Unity Manual: Normal Map and Mask Map for Sprites&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/Manual/urp/2d-lights-optimize-methods.html" rel="noopener noreferrer"&gt;Unity Manual: Optimize 2D lights&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.4/manual/Custom-Function-Node.html" rel="noopener noreferrer"&gt;Shader Graph: Custom Function Node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.4/manual/Dither-Node.html" rel="noopener noreferrer"&gt;Shader Graph: Dither Node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.4/manual/Sprite-Skinning-Node.html" rel="noopener noreferrer"&gt;Shader Graph: Sprite Skinning Node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.4/manual/Property-Types.html" rel="noopener noreferrer"&gt;Shader Graph: Property Types and Scope&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/Manual/gpu-instancing-change-data.html" rel="noopener noreferrer"&gt;Unity Manual: MaterialPropertyBlock and SRP Batcher&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/Manual/sprite/atlas/sprite-atlas-reference.html" rel="noopener noreferrer"&gt;Unity Manual: Sprite Atlas reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/ScriptReference/Sprite-bounds.html" rel="noopener noreferrer"&gt;Unity Scripting API: Sprite.bounds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/ScriptReference/Sprite-rect.html" rel="noopener noreferrer"&gt;Unity Scripting API: Sprite.rect&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/ScriptReference/Sprite-pixelsPerUnit.html" rel="noopener noreferrer"&gt;Unity Scripting API: Sprite.pixelsPerUnit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/Manual/urp/prebuilt-shader-graphs-urp-sprite-unlit.html" rel="noopener noreferrer"&gt;Unity Manual: Sprite Unlit Shader Graph&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.4/Documentation/Manual/texture-type-sprite.html" rel="noopener noreferrer"&gt;Unity Manual: Sprite texture Import Settings&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Sources for the distributed implementation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/gamedevtoollab-blip/unity-2d-shader-graph-effects/tree/c12a8ab0737b92029746795ec22f178c831ed3fd/Assets/ShaderCapture/Scripts/Editor" rel="noopener noreferrer"&gt;Generation code at the pinned revision&lt;/a&gt;: &lt;code&gt;ShaderCaptureProjectGenerator.cs&lt;/code&gt; defines the artwork, Scenes, and Bloom settings; &lt;code&gt;ShaderCaptureNativeGraphBuilder.cs&lt;/code&gt; defines the effect nodes; and &lt;code&gt;ShaderCaptureGraphGenerator.cs&lt;/code&gt; defines the Graph settings.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>shadergraph</category>
      <category>urp</category>
    </item>
    <item>
      <title>Real-Time Networking for 3D FPS Games in UE5: Replication, EOS, Dedicated Servers, and Pitfalls</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Fri, 11 Sep 2026 04:40:55 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/real-time-networking-for-3d-fps-games-in-ue5-replication-eos-dedicated-servers-and-pitfalls-1o0g</link>
      <guid>https://dev.to/gamedevtoollab/real-time-networking-for-3d-fps-games-in-ue5-replication-eos-dedicated-servers-and-pitfalls-1o0g</guid>
      <description>&lt;p&gt;Building an online FPS or TPS in Unreal Engine 5 is not a matter of choosing one "networking library." A production game usually has three networking layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gameplay networking&lt;/strong&gt; — movement, shooting, damage, ammo, projectiles, doors, objectives, and match state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Online services&lt;/strong&gt; — authentication, friends, invites, lobbies, sessions, matchmaking, voice, P2P, and relay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedicated-server operations&lt;/strong&gt; — allocation, region selection, health checks, deployment, logs, scaling, draining, and shutdown.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For most UE shooters, start with built-in Replication, RPCs, and &lt;code&gt;UCharacterMovementComponent&lt;/code&gt;. Use a Dedicated Server when fairness matters; consider a Listen Server for small co-op. Add Steam/EOS, GAS, Replication Graph, Iris, or external SDKs only when targets or measurements justify them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Technical baseline: August 31, 2026. The article uses UE 5.8 documentation and public UE 5.8.1/5.8.2 hotfix notes. The Network Emulation caveat below comes from source inspection of UE 5.8.0 CL &lt;code&gt;55116800&lt;/code&gt;; that engine build was not re-run while preparing this English edition.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Think in three layers, not one library
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Gameplay networking
&lt;/h3&gt;

&lt;p&gt;This layer synchronizes gameplay under latency, packet loss, reordering, relevance filtering, and bandwidth limits. UE Replication is a strong default because Actor ownership, per-connection relevancy, priority, dormancy, RPCs, property replication, and Character Movement prediction all live in the same networking model [1-9].&lt;/p&gt;

&lt;h3&gt;
  
  
  Online services
&lt;/h3&gt;

&lt;p&gt;Steam and EOS answer questions such as "who is the player?", "which lobby are they in?", and "where should they connect?" They do not replace Actor replication after the connection is established. In UE 5.8, Online Services is still documented as Beta, so projects shipping soon should evaluate it against the established Online Subsystem path [17-21].&lt;/p&gt;

&lt;h3&gt;
  
  
  Server operations
&lt;/h3&gt;

&lt;p&gt;A Dedicated Server still needs to be started, allocated, monitored, updated, drained, and terminated. Amazon GameLift Servers, PlayFab Multiplayer Servers, and Agones help with this operational layer [24-27]. They do not replace Replication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Competitive FPS: start from a server-authoritative Dedicated Server
&lt;/h2&gt;

&lt;p&gt;Unreal multiplayer is fundamentally client-server, with the server holding the authoritative world state [1,2]. That maps naturally to competitive shooters.&lt;/p&gt;

&lt;p&gt;A Listen Server can be perfectly reasonable for a two-to-four-player co-op game, but the host has direct access to the authoritative process. In PvP, that becomes a fairness problem in addition to host departure, NAT, and hardware variance.&lt;/p&gt;

&lt;p&gt;A Dedicated Server costs more to operate, but gives you a cleaner authoritative process for validation and consistent simulation. Choose by measured Actors, AI, shot frequency, server frame time, and bandwidth—not player count alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put replicated state in the right gameplay class
&lt;/h2&gt;

&lt;p&gt;A common Dedicated Server migration bug is trying to read server-only objects from client UI.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GameMode&lt;/code&gt;: server-only rules such as win conditions, spawn rules, admission rules.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GameState&lt;/code&gt;: public match state such as phase, team score, and round-end server time.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PlayerState&lt;/code&gt;: public per-player data such as team, kills, and deaths.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PlayerController&lt;/code&gt;: input, owner-only UI, and owner-only notifications.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Pawn&lt;/code&gt; / &lt;code&gt;Character&lt;/code&gt;: the current body, movement, and weapon interaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;GameMode&lt;/code&gt; does not exist on clients. &lt;code&gt;PlayerController&lt;/code&gt; is normally relevant only to its owning client, so it is also the wrong place for a scoreboard that everybody must see [31,32].&lt;/p&gt;

&lt;p&gt;For countdowns, replicate a &lt;strong&gt;round end server timestamp&lt;/strong&gt;, not "seconds remaining" every second. Clients can derive the display from &lt;code&gt;AGameStateBase::GetServerWorldTimeSeconds()&lt;/code&gt; [31,35], including after late join.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use &lt;code&gt;UCharacterMovementComponent&lt;/code&gt; before inventing movement RPCs
&lt;/h2&gt;

&lt;p&gt;Do not start an FPS by sending your own transform or velocity RPC every frame.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UCharacterMovementComponent&lt;/code&gt; already implements local prediction, saved moves, server simulation, correction, and smoothing for remote Characters [5]. Extend movement modes and saved moves for sprinting, sliding, or similar mechanics instead of creating a second synchronization system on top of it.&lt;/p&gt;

&lt;p&gt;For unusual vehicles or physics, custom prediction or the Network Prediction plugin may be relevant, but the UE 5.8 API still marks that plugin Beta [12]. Prototype before depending on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate state from events
&lt;/h2&gt;

&lt;p&gt;The most useful rule in UE networking is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;State that must be reconstructable&lt;/strong&gt; belongs in Replicated Properties.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transient events&lt;/strong&gt; may use RPCs when appropriate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HP, ammo, equipped weapon, death state, doors, score, and match phase should survive packet loss, late join, and becoming relevant again. That makes them state.&lt;/p&gt;

&lt;p&gt;Muzzle flashes, impact particles, and one-shot sounds may fit Unreliable RPCs, but prefer deriving visuals from replicated state when possible. Reliable RPCs are for low-frequency operations retried while the connection and channel remain valid; overuse can block later reliable traffic [3].&lt;/p&gt;

&lt;p&gt;Also remember ownership: a client can normally send a Server RPC only through an Actor it owns. Input RPCs therefore belong on the owning &lt;code&gt;PlayerController&lt;/code&gt;, Pawn, or an owned Component/Weapon whose owner chain is correct [3,32].&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not turn RPC ordering into a game rule
&lt;/h2&gt;

&lt;p&gt;UE documents representative ordering for one Actor under specific send/bunch conditions [33]. Do &lt;strong&gt;not&lt;/strong&gt; generalize that into a gameplay protocol across Actors, Components, frames, loss, or reordering.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ERemoteFunctionSendPolicy::ForceSend&lt;/code&gt; also has important limits: it is supported by Replication Graph or Iris, and only sends immediately when invoked in the supported &lt;code&gt;NetDriver::PostTickDispatch&lt;/code&gt; / &lt;code&gt;NetWorldTickTime&lt;/code&gt; phase. Outside that phase it behaves like &lt;code&gt;Default&lt;/code&gt;. &lt;code&gt;ForceQueue&lt;/code&gt; is different: it serializes later in the net update if bandwidth remains [3,33].&lt;/p&gt;

&lt;p&gt;Use sequence numbers, generations/epochs, and idempotent application logic instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server-authoritative shooting: separate intent from result
&lt;/h2&gt;

&lt;p&gt;The most dangerous shooter implementation is accepting &lt;code&gt;HitActor&lt;/code&gt;, bone, or damage values sent by the client.&lt;/p&gt;

&lt;p&gt;The client may send &lt;strong&gt;intent&lt;/strong&gt;: which weapon, aim direction, and a timestamp estimate. The server decides the result.&lt;/p&gt;

&lt;p&gt;The example below focuses on hitscan weapons. Slow projectiles, predicted projectiles, explosive overlap, and physics interaction need different designs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Give every shot a lifecycle-aware key
&lt;/h3&gt;

&lt;p&gt;A plain &lt;code&gt;uint16 Sequence&lt;/code&gt; is not enough if the weapon respawns, ownership changes, or a sequence wraps. Use a server-assigned source plus an epoch:&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;USTRUCT&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;FShotKey&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="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;uint32&lt;/span&gt; &lt;span class="n"&gt;FireSourceId&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="c1"&gt;// server assigned&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;uint16&lt;/span&gt; &lt;span class="n"&gt;FireEpoch&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="c1"&gt;// changes when the sequence space resets&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;uint16&lt;/span&gt; &lt;span class="n"&gt;Sequence&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="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;USTRUCT&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;FShotRequest&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="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;FShotKey&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;uint8&lt;/span&gt; &lt;span class="n"&gt;WeaponSlot&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;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;FVector_NetQuantizeNormal&lt;/span&gt; &lt;span class="n"&gt;AimDirection&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;uint32&lt;/span&gt; &lt;span class="n"&gt;EstimatedServerTimeMs&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before calling the Server RPC, the owning client records &lt;code&gt;(FireSourceId, FireEpoch, Sequence)&lt;/code&gt; in a &lt;strong&gt;client-process prediction history&lt;/strong&gt; and plays predicted local FX once. Do not tie that history to the current possession state. Keep it long enough to cover your maximum network/reconciliation delay, and namespace or clear it across disconnect, Server Travel, and session changes.&lt;/p&gt;

&lt;p&gt;This matters because a delayed multicast may arrive after death, unpossess, repossess, or controller transfer. Looking only at &lt;code&gt;IsLocallyControlled()&lt;/code&gt; at receive time is not enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validate the caller separately from the shot stream
&lt;/h3&gt;

&lt;p&gt;There are two trust questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which network connection actually sent this Server RPC?&lt;/li&gt;
&lt;li&gt;Is the &lt;code&gt;FireSourceId&lt;/code&gt; / epoch / slot in the request a stream that the server assigned to that connection?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not mix them.&lt;/p&gt;

&lt;p&gt;A useful conceptual flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CaptureOriginalCallerContext()
  -&amp;gt; ResolveReconciliationComponent(OriginalConnection)
  -&amp;gt; ValidateAssignedShotStream(Request, Caller)
  -&amp;gt; obtain CanonicalKey
  -&amp;gt; validate ammo/rate/time/aim
  -&amp;gt; authoritative trace
  -&amp;gt; PublishCanonicalShotResolution(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only a &lt;strong&gt;canonical&lt;/strong&gt;, server-validated shot key should enter the authoritative result cache or owner-only reconciliation snapshot.&lt;/p&gt;

&lt;p&gt;Treat invalid keys differently from ordinary gameplay rejection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unassigned source, future epoch, or a source belonging to another connection: audit/strike/disconnect policy; do not mutate result state.&lt;/li&gt;
&lt;li&gt;A stale epoch caused by an allowed switching race: optionally return one transient rejection to the original connection, but do not advance the authoritative snapshot.&lt;/li&gt;
&lt;li&gt;Valid key but empty magazine, rate limit, invalid timing window, or excessive aim delta: normal rejection; cache it like an accepted shot so duplicates stay idempotent.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Keep the result route tied to the original connection
&lt;/h3&gt;

&lt;p&gt;Do not resolve the destination again from the Weapon's &lt;em&gt;current&lt;/em&gt; owner when the result is ready. Resolve the reconciliation component from the connection captured at RPC dispatch time, ideally on an object that outlives the Pawn such as the original &lt;code&gt;PlayerController&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually:&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;UMyShotReconciliationComponent&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Reconciliation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;ResolveReconciliationComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Caller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OriginalConnection&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;TryGetCachedResolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CanonicalKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Cached&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Reconciliation&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;PublishCanonicalShotResolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Cached&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;ValidateGameplayShot&lt;/span&gt;&lt;span class="p"&gt;(...))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Reconciliation&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;PublishCanonicalShotResolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;BuildResolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CanonicalKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&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;Ammo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RejectReason&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;FServerShotResult&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RewindAndTraceOnServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ConsumeAmmoOnServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ApplyDamageOnServer&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;Reconciliation&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;PublishCanonicalShotResolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;BuildResolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CanonicalKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;true&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;bHitConfirmed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Ammo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="n"&gt;MulticastPlayConfirmedFireFx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CanonicalKey&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;CosmeticData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Duplicates return the cached result to the &lt;strong&gt;same original connection&lt;/strong&gt; without repeating ammo consumption, damage, snapshot mutation, or multicast.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reconciliation needs an immediate path and a state path
&lt;/h3&gt;

&lt;p&gt;An Unreliable Client RPC is useful for low-latency feedback, but it can be lost. Keep a compact owner-only replicated state as the convergence path.&lt;/p&gt;

&lt;p&gt;For example, track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FireSourceId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FireEpoch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LastContiguousResolvedSequence&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;a small recent accepted/rejected/hit detail window&lt;/li&gt;
&lt;li&gt;authoritative ammo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Register it as owner-only replication; the &lt;code&gt;UPROPERTY&lt;/code&gt; declaration alone does not do that:&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;UMyShotReconciliationComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GetLifetimeReplicatedProps&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;FLifetimeProperty&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;OutLifetimeProps&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;Super&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GetLifetimeReplicatedProps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OutLifetimeProps&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;DOREPLIFETIME_CONDITION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;UMyShotReconciliationComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ShotSyncState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;COND_OwnerOnly&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the reconciliation object is a replicated Component/Subobject, also configure its actual replication/registration path [4,16].&lt;/p&gt;

&lt;h3&gt;
  
  
  Predicted owner FX and confirmed remote FX are different responsibilities
&lt;/h3&gt;

&lt;p&gt;The owner already played the shot immediately. The multicast mainly exists for other clients.&lt;/p&gt;

&lt;p&gt;Use a client-process &lt;code&gt;PredictedFxHistory&lt;/code&gt; to suppress a confirmed FX key that this local process already predicted, and a separate &lt;code&gt;ConfirmedFxHistory&lt;/code&gt; to make remote FX idempotent. Perform that check in an application handler that does &lt;strong&gt;not&lt;/strong&gt; depend on current possession:&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;AMyWeapon&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ApplyConfirmedFxIfNew&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;FShotKey&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FFireFxData&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;FxData&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;GetClientShotFxRegistry&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;WasPredictedByAnyLocalPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="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;ConfirmedFxHistory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RememberIfNew&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="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;PlayConfirmedFireFx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FxData&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 survives delayed multicast delivery across death, unpossess, repossess, and ownership transfer; non-predicting AI/weapons simply play confirmed FX once.&lt;/p&gt;

&lt;p&gt;A useful regression test delays each client's application-handler entry &lt;strong&gt;after network delivery but before the prediction-history check&lt;/strong&gt;, changes possession/ownership, then resumes. The history-based implementation should still produce one total FX per client. A mutation that replaces the history check with &lt;code&gt;IsLocallyControlled()&lt;/code&gt; should fail. Delaying only &lt;code&gt;PlayConfirmedFireFx()&lt;/code&gt; is too late to catch that bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  The handlers must be idempotent
&lt;/h3&gt;

&lt;p&gt;Your design should remain correct for representative paths such as:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Owner FX&lt;/th&gt;
&lt;th&gt;Result convergence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client RPC arrives first&lt;/td&gt;
&lt;td&gt;1 predicted FX&lt;/td&gt;
&lt;td&gt;Immediate result; later property is harmless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Property arrives first&lt;/td&gt;
&lt;td&gt;1 predicted FX&lt;/td&gt;
&lt;td&gt;Snapshot resolves; later RPC is harmless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multicast arrives first&lt;/td&gt;
&lt;td&gt;Still 1 predicted FX&lt;/td&gt;
&lt;td&gt;Later RPC/property resolves the shot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server RPC request is lost&lt;/td&gt;
&lt;td&gt;1 predicted FX&lt;/td&gt;
&lt;td&gt;Later sequence/heartbeat or owner timeout cancels provisional state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client result RPC is lost&lt;/td&gt;
&lt;td&gt;1 predicted FX&lt;/td&gt;
&lt;td&gt;Owner-only property converges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multicast is lost&lt;/td&gt;
&lt;td&gt;Owner still has predicted FX&lt;/td&gt;
&lt;td&gt;Authoritative damage/ammo remain correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same game handler injected twice&lt;/td&gt;
&lt;td&gt;At most one FX&lt;/td&gt;
&lt;td&gt;Ammo/damage/result apply once&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Packet duplication does not prove the same gameplay RPC reached application code twice. Test idempotence by directly injecting the same key twice into authority, result, and FX handlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aim validation and lag compensation
&lt;/h2&gt;

&lt;p&gt;Checking that an aim vector is normalized is not enough.&lt;/p&gt;

&lt;p&gt;Reconstruct the trace origin from server-known historical view/Pawn/weapon state, not a client position. Compare the submitted direction with the accepted server view using state-specific tolerance for hip fire, ADS, recoil, view switching, turrets, or vehicles; also validate range, spread, interval, ammo, reload, and equipment. This constrains impossible trajectories, not all aimbots.&lt;/p&gt;

&lt;p&gt;For lag compensation, keep only needed history. Player hitboxes are common, while doors, shields, lifts, and destructible cover may also require historical collision state. Prefer a &lt;strong&gt;non-replicated historical collision proxy&lt;/strong&gt; over moving live Actors. If real Components are rewound, use a scoped guard that restores transform/collision on every exit path and does not emit extra overlaps, gameplay events, physics wake-ups, or replication dirtiness.&lt;/p&gt;

&lt;p&gt;For timestamps, &lt;code&gt;GetServerWorldTimeSeconds()&lt;/code&gt; is a useful synchronization entry point, but do not blindly trust a client value. Account for RTT, jitter, outliers, reconnects, Server Travel, and long sessions [35]. A match-relative quantized integer tick or &lt;code&gt;double&lt;/code&gt; is safer than an ever-growing absolute &lt;code&gt;float&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic weapons: design failure modes, not only happy paths
&lt;/h2&gt;

&lt;p&gt;There is no single correct fire protocol.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Benefit&lt;/th&gt;
&lt;th&gt;Main failure mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reliable start/stop&lt;/td&gt;
&lt;td&gt;Low message count, easy server timer&lt;/td&gt;
&lt;td&gt;Stop can wait behind reliable traffic; add state number, max duration, and forced stop on death/weapon change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unreliable input state + heartbeat&lt;/td&gt;
&lt;td&gt;Converges on later heartbeat&lt;/td&gt;
&lt;td&gt;Use sequence + timeout to discard stale input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unreliable request per shot&lt;/td&gt;
&lt;td&gt;Easy per-shot aim/result validation&lt;/td&gt;
&lt;td&gt;Lost shot request means that shot never happened; high ROF increases traffic and reconciliation work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Choose by fire rate, aim-update needs, packet-loss behavior, reliable-queue pressure, and cheat resistance.&lt;/p&gt;

&lt;h2&gt;
  
  
  GAS, Replication Graph, and Iris solve different problems
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GAS
&lt;/h3&gt;

&lt;p&gt;Gameplay Ability System is valuable when abilities, attributes, effects, tags, cooldowns, costs, and prediction need one model [13,14]. A rifle-focused FPS may not need it everywhere; using normal gunplay Components plus GAS for complex abilities can be simpler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replication Graph
&lt;/h3&gt;

&lt;p&gt;Replication Graph targets large Actor/connection counts by reusing persistent graph nodes for candidate selection [10]. Add it when relevance selection is measured as a bottleneck, after fixing unnecessary replication, relevancy/cull distance, update frequency, dormancy, priority, and data layout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Iris
&lt;/h3&gt;

&lt;p&gt;UE 5.8 documentation is awkward here: the 5.8 release notes call Iris production-ready, while UE 5.8 Iris pages still display Experimental warnings. Iris also remains opt-in [11,34]. Treat the exact engine patch, platform, plugins, replicated Subobjects, serializers, RepNotify behavior, Fast Arrays, and owner-only conditions as a regression-test matrix.&lt;/p&gt;

&lt;p&gt;And one very important rule: &lt;strong&gt;Iris and Replication Graph are not used together on the same NetDriver.&lt;/strong&gt; Iris does not support Replication Graph; filtering and prioritization take over those responsibilities [37-39]. Compare a Generic Replication + Replication Graph configuration against an Iris configuration rather than "adding Iris on top."&lt;/p&gt;

&lt;h2&gt;
  
  
  Steam and EOS are the control plane, not gameplay replication
&lt;/h2&gt;

&lt;p&gt;For Steam-only PC games, Online Subsystem Steam can handle login/session/invite/presence, with Steam Sockets where appropriate [19,20]. For multi-store or cross-platform plans, EOS provides authentication, lobbies, sessions, stats, voice, and P2P-related services [17,18]. Keep vendor APIs behind a game-facing abstraction; Lyra's Common User plugin is a useful flow reference [21].&lt;/p&gt;

&lt;h2&gt;
  
  
  Photon Fusion for Unreal is a different authority model
&lt;/h2&gt;

&lt;p&gt;Photon Fusion for Unreal 3 uses Shared Authority. That can be attractive for some co-op or distributed-authority games, but it is not the same model as one authoritative UE Dedicated Server [22].&lt;/p&gt;

&lt;p&gt;Fusion still reuses replicated/RepNotify-style properties and UE Character Networking [22,40], but logic/tooling built around one server &lt;code&gt;ROLE_Authority&lt;/code&gt; needs deeper redesign. At the August 31, 2026 baseline, Fusion for Unreal 3.0.0 is Preview; Windows/Android are supported, macOS/iOS are insufficiently tested, and console/Linux support is planned [23].&lt;/p&gt;

&lt;h2&gt;
  
  
  Use HTTP, WebSocket, and gRPC for backend work
&lt;/h2&gt;

&lt;p&gt;Use HTTP, WebSocket, or gRPC for accounts, inventory, matchmaking, allocation, results, telemetry, and LiveOps—not per-frame Character Movement or shooting. Replacing NetDriver gameplay traffic means rebuilding UE networking behavior. Keep trusted databases/services behind server-side APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dedicated-server hosting choices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon GameLift Servers&lt;/strong&gt;: placement, process integration, health, and scaling [24].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PlayFab Multiplayer Servers&lt;/strong&gt;: PlayFab-friendly hosting with an Unreal GSDK in the server build [25].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agones&lt;/strong&gt;: Kubernetes-native GameServer/Fleet/Allocation; its Unreal Client SDK/plugin belongs in the Dedicated Server process [26,27].&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compare regions, startup/warm pools, UDP/ports, rollout/rollback, autoscaling, DDoS boundaries, observability, crash handling, lock-in, and team expertise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimize bandwidth by not sending data
&lt;/h2&gt;

&lt;p&gt;Before compression, reduce who receives what.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Relevancy&lt;/strong&gt; — owner relevancy, cull distance, conditional replication [4,6,9].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update frequency&lt;/strong&gt; — tune by Actor role; it is not the server simulation tick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dormancy&lt;/strong&gt; — wake/flush &lt;em&gt;before&lt;/em&gt; changing replicated state [7].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Priority&lt;/strong&gt; — relative scheduling under saturation, not total bandwidth [8].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Representation&lt;/strong&gt; — quantize data and send IDs/state instead of verbose payloads.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fast Array Serializer helps with changing collections such as inventory/status effects [15]; do not treat replicated order as UI sort order. For replicated Subobjects, manage registration/removal carefully so replication never touches an invalid reference; Iris adds Subobject/fragment considerations [16,37].&lt;/p&gt;

&lt;h2&gt;
  
  
  Cheat resistance starts at the Server RPC boundary
&lt;/h2&gt;

&lt;p&gt;Validate ownership, legal state transitions, ammo/rate/speed/distance, authoritative or historical spatial state, RPC frequency, and malformed values such as NaN, invalid enums, oversized arrays, and unknown IDs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WithValidation&lt;/code&gt; failure can disconnect the caller [3]. Here, structural/protocol abuse is separated from ordinary latency/state-race rejection; that is a project policy, not a universal UE rule. Never embed service, admin, or database secrets in the player build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat bad networks as a normal test environment
&lt;/h2&gt;

&lt;p&gt;PIE with two local windows is not a networking test plan.&lt;/p&gt;

&lt;p&gt;Also test separate processes, packaged Client/Server builds, different machines/VMs, real online-service accounts, travel/reconnect, and bot load.&lt;/p&gt;

&lt;h3&gt;
  
  
  Be careful with &lt;code&gt;DropUnreliableRPC&lt;/code&gt; in UE 5.8.0
&lt;/h3&gt;

&lt;p&gt;The official command reference documents &lt;code&gt;NetEmulation.DropUnreliableRPC &amp;lt;RPCName&amp;gt; &amp;lt;0-100&amp;gt;&lt;/code&gt; [36]. Source inspection of Launcher UE 5.8.0 CL &lt;code&gt;55116800&lt;/code&gt; found a contradictory percentage/block path, so &lt;code&gt;100&lt;/code&gt; should not be used as proof that 100% of the target RPC was dropped. The public UE 5.8.1/5.8.2 hotfix lists do not identify a fix for that command [41,42].&lt;/p&gt;

&lt;p&gt;Therefore, for deterministic acceptance tests, prefer a &lt;strong&gt;test-build-only project hook&lt;/strong&gt; that explicitly skips the target call.&lt;/p&gt;

&lt;p&gt;For example, a server-side test CVar can skip only &lt;code&gt;ClientResolveShot&lt;/code&gt; while still updating the owner-only snapshot:&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;#if WITH_DEV_AUTOMATION_TESTS
&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;TAutoConsoleVariable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CVarSkipClientResolveShotForTest&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;"game.NetTest.SkipClientResolveShot"&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;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"1 skips ClientResolveShot in test builds."&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;Then a property-only convergence test should prove that the client actually had predictions to resolve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100 local predictions enqueued
Pending before snapshot apply == 100
Authority results == 100
ClientResolveShot received == 0
Snapshot source/epoch == test source/epoch
Snapshot contiguous prefix reaches the final issued sequence
Pending removed by snapshot == 100
Pending removed by timeout == 0
Pending after snapshot apply == 0
Displayed ammo == authoritative ammo
CVar/barrier/timeout settings restored during teardown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Freeze prediction timeout while the snapshot is deliberately blocked, otherwise timeout removal can create a false positive.&lt;/p&gt;

&lt;p&gt;For transport degradation, &lt;code&gt;PktLoss&lt;/code&gt;, &lt;code&gt;PktIncomingLoss&lt;/code&gt;, &lt;code&gt;PktDup&lt;/code&gt;, and &lt;code&gt;PktOrder&lt;/code&gt; are useful [28], but inject duplicate keys directly to test gameplay-handler idempotence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Networking Insights before changing architecture
&lt;/h2&gt;

&lt;p&gt;Use Networking Insights/Network Profiler to identify expensive Actors, properties, and RPCs [29,30]. "Networking is heavy" is not actionable; "this UI array replicates every tick to every connection" is.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical starting matrix
&lt;/h2&gt;

&lt;p&gt;These are &lt;strong&gt;evaluation starting points, not Unreal Engine limits&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Game shape&lt;/th&gt;
&lt;th&gt;Baseline to prototype&lt;/th&gt;
&lt;th&gt;Evaluate next&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2-4 player co-op&lt;/td&gt;
&lt;td&gt;Built-in Replication + Character Movement + Listen Server&lt;/td&gt;
&lt;td&gt;Steam/EOS P2P/relay, whether host migration is worth it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8-16 player PvP&lt;/td&gt;
&lt;td&gt;Dedicated Server + built-in Replication&lt;/td&gt;
&lt;td&gt;GAS, bounded lag compensation, hosting platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32-100 players / larger maps&lt;/td&gt;
&lt;td&gt;Dedicated Server + spatial relevancy&lt;/td&gt;
&lt;td&gt;Generic + Replication Graph &lt;strong&gt;or&lt;/strong&gt; Iris, Fast Arrays, autoscaling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Very large Actor counts&lt;/td&gt;
&lt;td&gt;Dedicated Server + bot load tests&lt;/td&gt;
&lt;td&gt;Compare Replication Graph and Iris as separate NetDriver configurations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Implementation order I would use
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Connect two clients to a minimal Dedicated Server C++ project.&lt;/li&gt;
&lt;li&gt;Make movement, possession, death, respawn, HP, weapon, and ammo correct.&lt;/li&gt;
&lt;li&gt;Split shooting into local prediction and authoritative resolution; add ownership checks, validation, and rate limits.&lt;/li&gt;
&lt;li&gt;Fix late join and relevancy re-entry, then make latency/loss tests routine.&lt;/li&gt;
&lt;li&gt;Add Steam/EOS login, sessions, and invites.&lt;/li&gt;
&lt;li&gt;Run a packaged server remotely and load it with bots while measuring frame time and bandwidth.&lt;/li&gt;
&lt;li&gt;Evaluate GAS, Replication Graph, or Iris only for measured problems.&lt;/li&gt;
&lt;li&gt;Integrate allocation, health, and shutdown with the hosting platform.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Start gameplay synchronization with UE Replication; treat Steam/EOS as online services and GameLift/PlayFab/Agones as server operations.&lt;/li&gt;
&lt;li&gt;Let &lt;code&gt;UCharacterMovementComponent&lt;/code&gt; handle normal Character prediction; replicate reconstructable state and use transient RPCs selectively.&lt;/li&gt;
&lt;li&gt;Make shooting server-authoritative, keyed by source/epoch/sequence, with an immediate owner result plus owner-only replicated convergence state.&lt;/li&gt;
&lt;li&gt;Keep predicted owner FX separate from confirmed remote FX, and make every entry point idempotent.&lt;/li&gt;
&lt;li&gt;Add GAS, Replication Graph, Iris, or external networking SDKs only after fixed-version load tests show what problem you are actually solving.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/networking-and-multiplayer-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Networking and Multiplayer in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/setting-up-dedicated-servers-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Setting Up Dedicated Servers in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/remote-procedure-calls-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Remote Procedure Calls in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/replicate-actor-properties-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Replicate Actor Properties in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/understanding-networked-movement-in-the-character-movement-component-for-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Understanding Networked Movement in the Character Movement Component&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/actor-relevancy-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Actor Relevancy in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/actor-network-dormancy-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Actor Network Dormancy in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/actor-priority-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Actor Priority in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/detailed-actor-replication-flow-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Detailed Actor Replication Flow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/replication-graph-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Replication Graph in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/iris-replication-system-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Iris Replication System in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/PluginIndex/NetworkPrediction?application_version=5.8" rel="noopener noreferrer"&gt;Network Prediction API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/using-gameplay-abilities-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Using Gameplay Abilities in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/abilities-in-lyra-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Abilities in Lyra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/NetCore/FFastArraySerializer?application_version=5.8" rel="noopener noreferrer"&gt;Fast Array Serializer API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/replicating-uobjects-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Replicating UObjects in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/overview-of-online-services-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Overview of Online Services in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/online-subsystem-eos-plugin-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Online Subsystem EOS Plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/online-subsystem-steam-interface-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Online Subsystem Steam&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/using-steam-sockets-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Using Steam Sockets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/common-user-plugin-in-unreal-engine-for-lyra-sample-game?application_version=5.8" rel="noopener noreferrer"&gt;Common User Plugin in Lyra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.photonengine.com/fusion-unreal/v3/fusion-intro" rel="noopener noreferrer"&gt;Photon Fusion for Unreal Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.photonengine.com/fusion-unreal/v3/getting-started/sdk-download" rel="noopener noreferrer"&gt;Photon Fusion for Unreal SDK Download&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/gameliftservers/latest/developerguide/integration-engines-setup-unreal.html" rel="noopener noreferrer"&gt;Amazon GameLift Servers Unreal Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/xbox/playfab/multiplayer/servers/server-sdks/unreal-gsdk/" rel="noopener noreferrer"&gt;PlayFab Multiplayer Servers Unreal GSDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://agones.dev/site/docs/overview/" rel="noopener noreferrer"&gt;Agones Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://agones.dev/site/docs/guides/client-sdks/unreal/" rel="noopener noreferrer"&gt;Agones Unreal Client SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/using-network-emulation-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Using Network Emulation in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/networking-insights-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Networking Insights in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/using-the-network-profiler-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Using the Network Profiler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/game-mode-and-game-state-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Game Mode and Game State in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/actor-owner-and-owning-connection-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Actor Owner and Owning Connection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/replicated-object-execution-order-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Replicated Object Execution Order&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-5-8-release-notes?application_version=5.8" rel="noopener noreferrer"&gt;Unreal Engine 5.8 Release Notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/BlueprintAPI/GameState/GetServerWorldTimeSeconds?application_version=5.8" rel="noopener noreferrer"&gt;Get Server World Time Seconds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-console-commands-reference?application_version=5.8" rel="noopener noreferrer"&gt;Unreal Engine Console Commands Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/migrate-to-iris-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Migrate to Iris in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/iris-filtering-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Iris Filtering in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/documentation/unreal-engine/iris-prioritization-in-unreal-engine?application_version=5.8" rel="noopener noreferrer"&gt;Iris Prioritization in Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.photonengine.com/fusion-unreal/v3/manual/replication/replicated-properties" rel="noopener noreferrer"&gt;Photon Fusion Unreal Property Replication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forums.unrealengine.com/t/5-8-1-hotfix-released/2738864" rel="noopener noreferrer"&gt;UE 5.8.1 Hotfix Released&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forums.unrealengine.com/t/5-8-2-hotfix-released/2746335" rel="noopener noreferrer"&gt;UE 5.8.2 Hotfix Released&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>unrealengine</category>
      <category>gamedev</category>
      <category>multiplayer</category>
      <category>cpp</category>
    </item>
    <item>
      <title>uGUI or UI Toolkit? A Practical Unity 6.5 UI Selection Guide</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Wed, 09 Sep 2026 00:44:52 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/ugui-or-ui-toolkit-a-practical-unity-65-ui-selection-guide-3i27</link>
      <guid>https://dev.to/gamedevtoollab/ugui-or-ui-toolkit-a-practical-unity-65-ui-selection-guide-3i27</guid>
      <description>&lt;p&gt;Choosing a UI system in Unity is not a simple "new UI Toolkit versus old uGUI" decision. Unity gives us three major options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;uGUI&lt;/strong&gt;: GameObject- and Component-based runtime UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI Toolkit&lt;/strong&gt;: UXML, USS, and C# for runtime and Editor UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IMGUI&lt;/strong&gt;: immediate-mode UI such as &lt;code&gt;OnGUI&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unity's 6.5 comparison still lists uGUI as the recommended general-purpose runtime UI, UI Toolkit as the runtime alternative, and UI Toolkit as the recommended Editor UI. That does not mean every runtime screen should use uGUI. A HUD, a 500-row inventory, a virtual joystick, a world-space panel, and an XR menu have different constraints.&lt;/p&gt;

&lt;p&gt;This article uses two stages: first apply &lt;strong&gt;hard gates&lt;/strong&gt; for version, render pipeline, input, XR, animation, and must-pass requirements; then compare only the surviving candidates using &lt;strong&gt;soft preferences&lt;/strong&gt; and identical prototypes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Version scope:&lt;/strong&gt; rechecked September 9, 2026. The article targets Unity 6.5 and uses &lt;code&gt;6000.5.11f1&lt;/code&gt;, the latest 6.5 patch then listed in the Unity Download Archive. Unity 6.3 LTS is included where its world-space workflow differs. Input System is &lt;code&gt;1.20.0&lt;/code&gt;; XR Interaction Toolkit is &lt;code&gt;3.6.0&lt;/code&gt;. In production, pin &lt;code&gt;ProjectVersion.txt&lt;/code&gt; and &lt;code&gt;packages-lock.json&lt;/code&gt; and rerun the tests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The short answer
&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;First candidate&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;HUD, virtual controls, Animator/Timeline-heavy presentation&lt;/td&gt;
&lt;td&gt;uGUI&lt;/td&gt;
&lt;td&gt;Strong Prefab, EventSystem, and animation workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inventory, settings, search, large lists&lt;/td&gt;
&lt;td&gt;UI Toolkit&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ListView&lt;/code&gt;, USS, reusable styling, flexible layout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;World-space or XR UI&lt;/td&gt;
&lt;td&gt;Prototype after hard gates&lt;/td&gt;
&lt;td&gt;Version and input route matter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editor Window / Inspector&lt;/td&gt;
&lt;td&gt;UI Toolkit&lt;/td&gt;
&lt;td&gt;Unity's recommended Editor UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Display-only runtime debug overlay&lt;/td&gt;
&lt;td&gt;IMGUI can be acceptable&lt;/td&gt;
&lt;td&gt;Small implementation for short-lived tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interactive Player debug UI&lt;/td&gt;
&lt;td&gt;uGUI or UI Toolkit&lt;/td&gt;
&lt;td&gt;Runtime IMGUI has Input System constraints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mature uGUI project&lt;/td&gt;
&lt;td&gt;Usually keep uGUI&lt;/td&gt;
&lt;td&gt;Avoid migration and mixed-stack regression cost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A useful default is: &lt;strong&gt;gameplay-, GameObject-, and keyframe-heavy UI favors uGUI; data-, list-, and style-heavy UI favors UI Toolkit; Editor tooling favors UI Toolkit; IMGUI stays narrow.&lt;/strong&gt; Hard gates override that default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply hard gates before preferences
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hard gate&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;If it fails&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unity version&lt;/td&gt;
&lt;td&gt;6.3 world space uses &lt;code&gt;UIDocument&lt;/code&gt;; 6.5 centers the workflow on &lt;code&gt;PanelRenderer&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Use documentation for the exact version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render pipeline&lt;/td&gt;
&lt;td&gt;UI Shader Graph in 6.5 is &lt;strong&gt;URP-only&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Built-in/HDRP need another shader/material route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;Virtual Mouse is uGUI-only; standard tracked input is not UI Toolkit XR input&lt;/td&gt;
&lt;td&gt;Use uGUI or the XRI-specific UI Toolkit path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animation&lt;/td&gt;
&lt;td&gt;PanelRenderer animation support is not general Timeline integration&lt;/td&gt;
&lt;td&gt;Prefer uGUI if Timeline is mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime IMGUI&lt;/td&gt;
&lt;td&gt;Input System-only Player input is incompatible with IMGUI&lt;/td&gt;
&lt;td&gt;Test &lt;code&gt;Both&lt;/code&gt; or move the tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product requirements&lt;/td&gt;
&lt;td&gt;XR hardware, RTL, text scaling, accessibility, focus, performance budget&lt;/td&gt;
&lt;td&gt;Do not adopt an unverified candidate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Record the Editor patch, packages, SRP, input route, build, and device before judging a result.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each UI system is good at
&lt;/h2&gt;

&lt;h3&gt;
  
  
  uGUI: scene-oriented runtime UI
&lt;/h3&gt;

&lt;p&gt;uGUI is built around &lt;code&gt;Canvas&lt;/code&gt;, &lt;code&gt;RectTransform&lt;/code&gt;, &lt;code&gt;Image&lt;/code&gt;, &lt;code&gt;TextMeshProUGUI&lt;/code&gt;, &lt;code&gt;Button&lt;/code&gt;, Scenes, and Prefabs. It fits Inspector/Prefab/Animator/Timeline workflows and is a strong default for HUDs, reward popups, combat indicators, virtual sticks, and heavily animated menus. Its main risk is architectural: logic and update work can spread across Prefabs, MonoBehaviours, Layout components, and Canvases.&lt;/p&gt;

&lt;h3&gt;
  
  
  UI Toolkit: structure, style, and retained state
&lt;/h3&gt;

&lt;p&gt;UI Toolkit uses UXML, USS, and C#. Its Flexbox-style layout is based on Yoga layout principles. It is attractive for inventories, filters, tables, settings, design systems, and repeated data; &lt;code&gt;ListView&lt;/code&gt; is particularly useful for large reusable lists. UXML/USS are not identical to HTML/CSS: Panel Settings, events, atlases, supported properties, and runtime features remain Unity-specific and version-sensitive.&lt;/p&gt;

&lt;h3&gt;
  
  
  IMGUI: keep it narrow
&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;OnGUI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GUILayout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Enemy Count: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;_enemyCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is fine for a display-only diagnostic or small existing Editor tool. For product UI or a long-lived interactive Player tool, uGUI or UI Toolkit usually gives a safer input, styling, reuse, and accessibility path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime and Editor are different decisions
&lt;/h2&gt;

&lt;p&gt;For &lt;strong&gt;runtime UI&lt;/strong&gt;, uGUI and UI Toolkit are both candidates after the hard gates. Unity 6.5 still recommends uGUI overall, while UI Toolkit can fit data- and style-heavy screens better.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;Editor UI&lt;/strong&gt;, start new Editor Windows and Custom Inspectors with UI Toolkit. A small stable IMGUI Editor tool does not need a rewrite. Editor IMGUI is also separate from Player &lt;code&gt;OnGUI&lt;/code&gt; compatibility with the Input System.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unity 6.3 and 6.5 use different world-space workflows
&lt;/h2&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;Unity 6.3 LTS&lt;/th&gt;
&lt;th&gt;Unity 6.5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Main world-space component&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UIDocument&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PanelRenderer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Size / pivot&lt;/td&gt;
&lt;td&gt;UI Document &lt;code&gt;World-Space Dimensions&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Panel Renderer &lt;code&gt;World-Space Dimensions&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple containers&lt;/td&gt;
&lt;td&gt;UI Documents sharing Panel Settings&lt;/td&gt;
&lt;td&gt;Panel Renderers sharing Panel Settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inspector references&lt;/td&gt;
&lt;td&gt;Do not assume &lt;code&gt;VisualElementReference&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Runtime &lt;code&gt;PanelRenderer&lt;/code&gt; can use it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legacy &lt;code&gt;UIDocument&lt;/code&gt; access&lt;/td&gt;
&lt;td&gt;UQuery / query-based&lt;/td&gt;
&lt;td&gt;Still query-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editor &lt;code&gt;VisualElementReference&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animator / legacy Animation&lt;/td&gt;
&lt;td&gt;Do not assume 6.5 support&lt;/td&gt;
&lt;td&gt;PanelRenderer content has dedicated support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;General Animation Clip / Timeline&lt;/td&gt;
&lt;td&gt;Do not assume it&lt;/td&gt;
&lt;td&gt;Comparison table still lists UI Toolkit as unsupported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;World-space ordering&lt;/td&gt;
&lt;td&gt;z distance, sibling &lt;code&gt;sortingOrder&lt;/code&gt;, camera culling&lt;/td&gt;
&lt;td&gt;Same; no 2D Sorting Layers integration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you are on 6.3, do not search for a &lt;code&gt;PanelRenderer&lt;/code&gt; from the 6.5 workflow. Use &lt;code&gt;UIDocument.rootVisualElement&lt;/code&gt; and the 6.3 UI Document path.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;VisualElementReference&amp;lt;T&amp;gt;&lt;/code&gt; is a &lt;strong&gt;6.5 runtime feature for &lt;code&gt;PanelRenderer&lt;/code&gt;&lt;/strong&gt;; it is not a universal replacement for UQuery in Editor UI or legacy &lt;code&gt;UIDocument&lt;/code&gt; setups.&lt;/p&gt;

&lt;p&gt;Unity 6000.5.0f1 also added Animator/legacy Animation support for PanelRenderer content. That does &lt;strong&gt;not&lt;/strong&gt; make UI Toolkit a general equivalent of uGUI's Animation Clip/Timeline workflow. If existing Timeline-authored UI is a hard requirement, favor uGUI unless a prototype proves the required path.&lt;/p&gt;

&lt;p&gt;World-space UI Toolkit also does not integrate with 2D Sorting Layers. Use camera-relative z distance, sibling &lt;code&gt;sortingOrder&lt;/code&gt; where applicable, and GameObject Layers plus camera culling. A design that depends on SpriteRenderer-style Sorting Layers needs a different plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  World-space rendering is not XR controller input
&lt;/h2&gt;

&lt;p&gt;A panel rendering in 3D space does not prove that a tracked VR controller can operate it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Standard Input System tracked input
&lt;/h3&gt;

&lt;p&gt;Input System 1.20 documents tracked UI input using &lt;code&gt;InputSystemUIInputModule&lt;/code&gt;, tracked position/orientation actions, and &lt;code&gt;TrackedDeviceRaycaster&lt;/code&gt; on a &lt;code&gt;Canvas&lt;/code&gt;. That is the standard tracked &lt;strong&gt;uGUI&lt;/strong&gt; route.&lt;/p&gt;

&lt;p&gt;UI Toolkit is different: Input System 1.20 states that tracked-type XR input is not supported with UI Toolkit, and UI Toolkit does not support &lt;code&gt;TrackedDeviceRaycaster&lt;/code&gt; because it performs its own raycasting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;InputSystemUIInputModule
  + trackedDevicePosition / trackedDeviceOrientation
  + TrackedDeviceRaycaster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not copy that Canvas route into UI Toolkit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Panel Input Configuration
&lt;/h3&gt;

&lt;p&gt;Unity 6.5 &lt;code&gt;Panel Input Configuration&lt;/code&gt; handles Event Camera, Physics Layer, maximum distance, and EventSystem redirection, and can create helpers such as &lt;code&gt;Panel Event Handler&lt;/code&gt;, &lt;code&gt;Panel Raycaster&lt;/code&gt;, and &lt;code&gt;World Document Raycaster&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its screen-based pointer route converts screen position through an Event Camera into a world-space ray. &lt;strong&gt;By itself, it is not a tracked XR-controller solution.&lt;/strong&gt; &lt;code&gt;World Document Raycaster&lt;/code&gt; is also not the Input System's &lt;code&gt;TrackedDeviceRaycaster&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  XRI 3.6 dedicated UI Toolkit integration
&lt;/h3&gt;

&lt;p&gt;This is what keeps UI Toolkit viable for VR. Pin XRI 3.6, import the &lt;strong&gt;World Space UI&lt;/strong&gt; sample, follow that package's setup, and validate on hardware. Test Ray, Poke, both controllers, focus, colliders, modal behavior, and latency. If uGUI and UI Toolkit coexist, also test event routing and focus ownership.&lt;/p&gt;

&lt;p&gt;The Unity 6.5 core manual and the XRI 3.6 sample can use different component arrangements. Follow the exact sample for the pinned package rather than mechanically replacing its components with names from newer core documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layout and information density
&lt;/h2&gt;

&lt;p&gt;For a fixed combat HUD, uGUI anchors and RectTransforms are often enough, especially when the project already has Safe Area helpers, localization Prefabs, tween scripts, and animation assets.&lt;/p&gt;

&lt;p&gt;For search, filters, multiple panes, reusable themes, text scaling, or hundreds of rows, prototype UI Toolkit first. &lt;code&gt;ListView&lt;/code&gt; is built around reusable items; a naive uGUI list with hundreds of active GameObjects usually needs explicit pooling and careful layout design.&lt;/p&gt;

&lt;p&gt;The real comparison is not "Flexbox versus RectTransform." It is the cost of the actual screen under realistic content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Animation and shaders
&lt;/h2&gt;

&lt;p&gt;If UI artists repeatedly tune Animator Controllers, Animation Clips, Timeline tracks, tween components, or UI particle assets, uGUI usually matches the production workflow better. UI Toolkit has USS transitions, class-driven state changes, and 6.5 PanelRenderer-specific animation, but that does not guarantee parity with an established uGUI pipeline.&lt;/p&gt;

&lt;p&gt;Ask: &lt;strong&gt;who changes the animation, in which tool, and how often?&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  UI Shader Graph is URP-only
&lt;/h3&gt;

&lt;p&gt;Unity 6.5 UI Shader Graph is a &lt;strong&gt;URP feature&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;URP&lt;/strong&gt;: prototype with the real masks, stencil behavior, blending, world-space setup, and target GPUs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in/HDRP&lt;/strong&gt;: remove UI Shader Graph from the plan and validate another custom Material/Shader route.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Render Pipeline belongs in both the hard-gate table and every benchmark record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Input
&lt;/h2&gt;

&lt;p&gt;Virtual sticks, multi-touch gestures, EventSystem extensions, and Virtual Mouse generally push the decision toward uGUI. Input System 1.20's Virtual Mouse path is for uGUI rather than a general UI Toolkit/IMGUI cursor abstraction.&lt;/p&gt;

&lt;p&gt;UI Toolkit also distinguishes pointer and navigation events. A pointer click raises &lt;code&gt;ClickEvent&lt;/code&gt;; a gamepad submit uses navigation events. For a &lt;code&gt;Button&lt;/code&gt; that should activate through either path, &lt;code&gt;Button.clicked&lt;/code&gt; can be a better semantic hook than listening only for &lt;code&gt;ClickEvent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Prototype focus order, device switching, focus restoration after a modal, drag/scroll, IME, software keyboard, local multiplayer, and back/cancel routing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime IMGUI with the Input System
&lt;/h3&gt;

&lt;p&gt;Input System 1.20 documents IMGUI as incompatible with the Input System package in the Player path. A display-only &lt;code&gt;OnGUI&lt;/code&gt; overlay can still be useful, but an interactive Player debug menu should not silently assume Input System-only will work.&lt;/p&gt;

&lt;p&gt;You can test &lt;strong&gt;Active Input Handling = Both&lt;/strong&gt;, but then verify duplicate processing and performance. Long-lived interactive Player tools are usually better in uGUI or UI Toolkit. Editor IMGUI is a separate case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Localization and accessibility
&lt;/h2&gt;

&lt;p&gt;Do not choose from an English mockup alone. Where relevant, make maximum text length, RTL, fallback fonts, 200% text scaling, non-color-only states, keyboard operation, and platform assistive technology must-pass requirements.&lt;/p&gt;

&lt;p&gt;Unity's Accessibility Hierarchy still needs deliberate names, roles, states, focus, and dynamic updates. Neither UI system makes accessibility automatic; validate on the target OS with the target assistive technology.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance: benchmark the update pattern, not the brand
&lt;/h2&gt;

&lt;p&gt;Use the same device, data set, animation, render pipeline, and Graphics API.&lt;/p&gt;

&lt;p&gt;For uGUI, Unity's optimization guidance emphasizes reducing expensive Canvas rebuilds, avoiding unnecessary layout work and raycast targets, and splitting Canvases by update behavior rather than arbitrary counts. Pool large repeated lists.&lt;/p&gt;

&lt;p&gt;For UI Toolkit, test real &lt;code&gt;ListView&lt;/code&gt; reuse, transforms, Dynamic Atlas, masks, transparency, and panel organization. Do not claim "more panels are always slower." Capture UI Toolkit-related Profiler data instead.&lt;/p&gt;

&lt;p&gt;Predefine the metrics and aggregation: CPU time, GC allocations, draw calls, memory, first-open time, open spike, scroll p95/worst, and input response. A benchmark without a fixed measurement interval is not reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mixing uGUI and UI Toolkit
&lt;/h2&gt;

&lt;p&gt;Mixing is possible, but creates two focus systems, styling models, ordering rules, modal interactions, input-blocking concerns, and extra QA. Prefer screen/navigation boundaries: for example, "HUD is uGUI; inventory is UI Toolkit." Avoid alternating technologies inside one dialog.&lt;/p&gt;

&lt;p&gt;Use a navigation service or presenter layer between systems rather than letting one hierarchy manipulate the other directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep game logic outside the View
&lt;/h2&gt;

&lt;p&gt;A migration becomes expensive when the View owns inventory rules, purchases, networking, saves, analytics, and transitions. Put a small interface at the 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;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IInventoryView&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ItemSelected&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;SetItems&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;InventoryItemViewModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetGold&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A presenter can depend on this interface and game-side services. Migration is not free, but the blast radius is smaller when the View implementation changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reproducible selection process
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Freeze the test matrix
&lt;/h3&gt;

&lt;p&gt;Record the exact Editor patch, package versions, SRP, Build Target, backend, Graphics API, device/OS, resolution, refresh rate, and thermal/power state. Different matrices are different tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Apply hard gates before soft preferences
&lt;/h3&gt;

&lt;p&gt;Reject unavailable components, unsupported pipelines, unusable input routes, and failed product requirements first. Preserve evidence for rejected candidates. Only surviving candidates should be compared for team skill, authoring speed, layout, animation workflow, maintenance, and migration cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Resolve conflicts deterministically
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Evaluate hard gates per candidate.&lt;/li&gt;
&lt;li&gt;If the UI can split at a screen/navigation boundary, prototype that mixed design including focus, modal, ordering, input blocking, and transitions.&lt;/li&gt;
&lt;li&gt;Otherwise compare uGUI and UI Toolkit on the &lt;strong&gt;same&lt;/strong&gt; screen, build, data set, device, and input sequence.&lt;/li&gt;
&lt;li&gt;Only candidates whose complete must-pass set is &lt;code&gt;pass&lt;/code&gt; enter the final cost/performance comparison.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pre-register the next action: &lt;code&gt;unknown&lt;/code&gt; means &lt;code&gt;retest&lt;/code&gt;; any &lt;code&gt;fail&lt;/code&gt; means &lt;code&gt;no-adopt&lt;/code&gt;; both candidates failing means &lt;code&gt;revise-requirement&lt;/code&gt;; one passing candidate can be adopted; two passing candidates are compared against a predefined equivalence band. A tie requires a more discriminating fixture, then a pre-registered final tie-breaker such as migration cost. Do not invent a metric after seeing the result.&lt;/p&gt;

&lt;p&gt;Prototype count is determined by coverage, not by a fixed number. Add fixtures for uncovered XR, RTL/text scaling/accessibility, mixed-stack focus/modal/back, shader/SRP, or mobile requirements. Every must-pass ID needs at least one procedure and one evidence artifact.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Keep candidate records and the ADR separate
&lt;/h3&gt;

&lt;p&gt;A candidate record should be immutable and reproducible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;record-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TR-UI-CMP-QUEST-UGUI-01&lt;/span&gt;
&lt;span class="na"&gt;comparison-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UI-CMP-QUEST-INVENTORY&lt;/span&gt;
&lt;span class="na"&gt;candidate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ugui&lt;/span&gt;

&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;unity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;6000.5.11f1&lt;/span&gt;
  &lt;span class="na"&gt;project-commit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;git-sha&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Android&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;IL2CPP&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;sha256&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;&amp;lt;sha256&amp;gt;&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;rendering&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;pipeline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;URP&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;graphics-api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Vulkan&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;device&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Meta Quest 3&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;refresh-hz&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;90&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="na"&gt;fixture&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Tests/UI/inventory-500.json&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;sha256&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;&amp;lt;sha256&amp;gt;&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;input-sequence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;scroll&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;modal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;left/right&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ray&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;poke"&lt;/span&gt;
  &lt;span class="na"&gt;random-seed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;48151623&lt;/span&gt;

&lt;span class="na"&gt;measurement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;warm-up&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;trials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
  &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inventory-ready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;through&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;scripted&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sequence"&lt;/span&gt;
  &lt;span class="na"&gt;outlier-policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;preserve&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;trials"&lt;/span&gt;
  &lt;span class="na"&gt;aggregation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;median&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;p95&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;worst&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;must-pass&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FUNC-FOCUS-001&lt;/span&gt;
    &lt;span class="na"&gt;criterion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Modal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;close&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;restores&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;previous&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;focus"&lt;/span&gt;
    &lt;span class="na"&gt;procedure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Select&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;open/close&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;modal,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;continue&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;navigation"&lt;/span&gt;
    &lt;span class="na"&gt;expected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Navigation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;resumes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;
    &lt;span class="na"&gt;actual&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30/30&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;restored&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;focus"&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pass&lt;/span&gt;
    &lt;span class="na"&gt;evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Tests/UI/evidence/focus.mp4&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;sha256&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;&amp;lt;sha256&amp;gt;&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each requirement needs &lt;code&gt;criterion&lt;/code&gt;, &lt;code&gt;procedure&lt;/code&gt;, &lt;code&gt;expected&lt;/code&gt;, &lt;code&gt;actual&lt;/code&gt;, &lt;code&gt;pass|fail|unknown&lt;/code&gt;, and evidence. Any &lt;code&gt;fail&lt;/code&gt; or &lt;code&gt;unknown&lt;/code&gt; makes the candidate ineligible. Also preserve build configuration, package versions, fixture hashes, warm-up, measurement interval, trial count, aggregation, actual metric values/units, and whether each budget is an &lt;code&gt;example&lt;/code&gt; or an &lt;code&gt;approved&lt;/code&gt; threshold with its basis and approver.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;ADR is another file&lt;/strong&gt;. It links all candidate record IDs/hashes and stores comparison scope, status, owner/date, alternatives, decision, rejection reasons, trade-offs, consequences, open issues, and revisit triggers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;adr-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ADR-UI-QUEST-INVENTORY&lt;/span&gt;
&lt;span class="na"&gt;comparison-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UI-CMP-QUEST-INVENTORY&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;accepted&lt;/span&gt;
&lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UI Platform Lead&lt;/span&gt;

&lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;adopt&lt;/span&gt;
  &lt;span class="na"&gt;selected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ugui&lt;/span&gt;
  &lt;span class="na"&gt;rationale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;All&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;must-pass&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;checks&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;passed;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;uGUI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;won&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;outside&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;predefined&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;band"&lt;/span&gt;

&lt;span class="na"&gt;revisit-triggers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Unity/Input System/XRI upgrade&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;item count exceeds &lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;XR interaction requirements change&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test record answers &lt;strong&gt;what happened under a reproducible build&lt;/strong&gt;. The ADR answers &lt;strong&gt;why this option won and when the decision must be reopened&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Let content authors test the workflow
&lt;/h3&gt;

&lt;p&gt;Have UI designers or technical artists change copy, spacing, colors, and animation. Record merge conflicts, diff readability, what they can edit without programmer help, and time per representative change. Authoring cost is architecture cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should an existing uGUI project migrate?
&lt;/h2&gt;

&lt;p&gt;Ask whether migration cost can be recovered, not whether migration is technically possible. Benefits can include shared styling, better large-list workflows, flexible layouts, fewer Prefab conflicts, and shared runtime/Editor concepts. Costs include reimplementation, input, focus, animation, localization, analytics, plugins, and regression QA.&lt;/p&gt;

&lt;p&gt;Migrate by screen boundary, preferably with an independent new screen. Avoid a broad rewrite when release is near, the current UI is stable, the alleged performance problem is unmeasured, a must-have path is unsupported, or mixed-stack QA is unfunded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final audit before adoption
&lt;/h2&gt;

&lt;p&gt;Retest if any of these are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the team chose from "uGUI is obsolete" or "UI Toolkit is automatically faster" without measurement;&lt;/li&gt;
&lt;li&gt;Unity 6.3 &lt;code&gt;UIDocument&lt;/code&gt; and 6.5 &lt;code&gt;PanelRenderer&lt;/code&gt; workflows are mixed;&lt;/li&gt;
&lt;li&gt;PanelRenderer animation is treated as general Timeline integration;&lt;/li&gt;
&lt;li&gt;UI Shader Graph is assumed outside URP;&lt;/li&gt;
&lt;li&gt;UI Toolkit XR uses standard tracked input or &lt;code&gt;TrackedDeviceRaycaster&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Panel Input Configuration&lt;/code&gt; alone is treated as the XR-controller solution;&lt;/li&gt;
&lt;li&gt;Runtime IMGUI input or mixed focus/modal behavior is untested;&lt;/li&gt;
&lt;li&gt;must-pass evidence, candidate records, thresholds, trial conditions, ADR, or revisit triggers are missing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice: &lt;strong&gt;uGUI fits gameplay/keyframe-heavy UI; UI Toolkit fits list/layout/style/Editor-heavy UI; IMGUI fits narrow development UI.&lt;/strong&gt; Production adoption should follow hard-gate success and reproducible target-device evidence.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://unity.com/releases/editor/archive" rel="noopener noreferrer"&gt;Unity Download Archive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/UI-system-compare.html" rel="noopener noreferrer"&gt;Unity 6.5: Comparison of UI systems in Unity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/ui-systems/create-world-space-ui.html" rel="noopener noreferrer"&gt;Unity 6.3: Create a World Space UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/ui-systems/create-world-space-ui.html" rel="noopener noreferrer"&gt;Unity 6.5: Create a World Space UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/ui-systems/reference-visual-elements-in-inspector.html" rel="noopener noreferrer"&gt;Unity 6.5: Reference runtime visual elements in Inspector&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/ui-systems/world-space-panel-input-configuration.html" rel="noopener noreferrer"&gt;Unity 6.5: Panel Input Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/ui-systems/ui-shader-graph.html" rel="noopener noreferrer"&gt;Unity 6.5: UI Shader Graph&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/UIE-LayoutEngine.html" rel="noopener noreferrer"&gt;Unity 6.5: Layout engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://unity.com/releases/editor/whats-new/6000.5.0f1" rel="noopener noreferrer"&gt;Unity 6000.5.0f1 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://unity.com/releases/editor/whats-new/6000.5.11f1" rel="noopener noreferrer"&gt;Unity 6000.5.11f1 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/com.unity.inputsystem.html" rel="noopener noreferrer"&gt;Unity 6.5: Input System package version&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.inputsystem@1.20/manual/understand-ui-compatibility.html" rel="noopener noreferrer"&gt;Input System 1.20: Understand UI system compatibility&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.inputsystem@1.20/manual/supported-ui-input-types-tracked.html" rel="noopener noreferrer"&gt;Input System 1.20: Tracked input UI support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.inputsystem@1.20/manual/virtual-mouse-ui-cursor-control.html" rel="noopener noreferrer"&gt;Input System 1.20: Virtual Mouse UI cursor control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.inputsystem@1.20/manual/use-imgui-alongside-input-system.html" rel="noopener noreferrer"&gt;Input System 1.20: Use IMGUI alongside the Input System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.inputsystem@1.20/manual/introduction-multiplayer-ui-input.html" rel="noopener noreferrer"&gt;Input System 1.20: Multiplayer UI input&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.xr.interaction.toolkit@3.6/manual/samples-world-space-ui.html" rel="noopener noreferrer"&gt;XRI 3.6: World Space UI sample&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://unity.com/how-to/unity-ui-optimization-tips" rel="noopener noreferrer"&gt;Unity: Optimization tips for Unity UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/UIE-uxml-element-ListView.html" rel="noopener noreferrer"&gt;Unity 6.5: ListView&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/best-practice-guides/ui-toolkit-for-advanced-unity-developers/optimizing-performance.html" rel="noopener noreferrer"&gt;Unity 6.5: Optimizing UI Toolkit performance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/UIE-Transitioning-From-UGUI.html" rel="noopener noreferrer"&gt;Unity 6.5: Transitioning from uGUI to UI Toolkit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/accessibility/module-intro.html" rel="noopener noreferrer"&gt;Unity 6.5: Accessibility module&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>ui</category>
      <category>csharp</category>
    </item>
    <item>
      <title>How Optimized Is Unity Shader Graph in Unity 6? Where HLSL Still Wins</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Tue, 08 Sep 2026 02:11:03 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/how-optimized-is-unity-shader-graph-in-unity-6-where-hlsl-still-wins-4222</link>
      <guid>https://dev.to/gamedevtoollab/how-optimized-is-unity-shader-graph-in-unity-6-where-hlsl-still-wins-4222</guid>
      <description>&lt;p&gt;A few years ago, I treated Shader Graph as a prototyping tool. When a shader became performance-sensitive, I often rewrote the bottleneck in HLSL because stage placement, precision, branching, reuse, and pass structure were easier to control by hand.&lt;/p&gt;

&lt;p&gt;So how much of that old rule still applies?&lt;/p&gt;

&lt;p&gt;This article uses &lt;strong&gt;Unity 6.3 LTS (6000.3) and Shader Graph 17.3&lt;/strong&gt; as the baseline: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/com.unity.shadergraph.html" rel="noopener noreferrer"&gt;Shader Graph package information&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;For ordinary URP and HDRP materials, starting with Shader Graph is a reasonable default. Do not rewrite a finished graph in HLSL just because it is a graph. Inspect the generated shader and measure the target GPU first.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If Shader Graph and handwritten HLSL express the same work in the same stage, with the same precision, samples, branches, passes, and pipeline features, both eventually enter the platform shader compiler. Their final code may be similar, though identical output is not guaranteed.&lt;/p&gt;

&lt;p&gt;The useful question is therefore: &lt;strong&gt;what work is executed, in which stage, how many times, and across how many passes and variants?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is not a fixed-hardware benchmark. It is a guide to what Shader Graph generates, what it does not optimize for you, and when profiling should push you toward Custom Function or handwritten ShaderLab/HLSL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three layers you should not confuse
&lt;/h2&gt;

&lt;p&gt;Separate &lt;strong&gt;the graph&lt;/strong&gt;, &lt;strong&gt;Unity-generated ShaderLab/HLSL&lt;/strong&gt;, and &lt;strong&gt;the platform-compiled GPU code&lt;/strong&gt;. Nodes are not runtime objects executed one by one; Shader Graph generates source, then normal shader compilation takes over.&lt;/p&gt;

&lt;p&gt;That is why a 100-node graph does not mean 100 runtime function calls, and why a long generated file is not automatically slow. Pipeline templates, varyings, keywords, lighting integration, and helper code can inflate source length. Conversely, a tiny handwritten shader can still be expensive if it performs many texture reads, screen-space operations, or loops.&lt;/p&gt;

&lt;p&gt;Compare generated passes/stages, texture samples, branch structure, varyings/register pressure, variant count, warm-up, and finally GPU time on the target device.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Shader Graph optimizes today
&lt;/h2&gt;

&lt;p&gt;Modern Shader Graph is best treated as a &lt;strong&gt;shader code generator and authoring system&lt;/strong&gt;, not a runtime abstraction layer.&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;What it can do&lt;/th&gt;
&lt;th&gt;What it will not do for you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shader Graph generation&lt;/td&gt;
&lt;td&gt;Trace dependencies, generate per-pass/per-stage code, emit fields/varyings&lt;/td&gt;
&lt;td&gt;Redesign your algorithm, always choose the optimal stage, merge every redundant sample&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HLSL compilation&lt;/td&gt;
&lt;td&gt;Constant folding, dead-code elimination, inlining&lt;/td&gt;
&lt;td&gt;Produce identical machine code on every GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Driver/backend&lt;/td&gt;
&lt;td&gt;Generate target-specific instructions&lt;/td&gt;
&lt;td&gt;Guarantee the same register/instruction count as handwritten HLSL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Unconnected nodes normally do not become runtime work
&lt;/h3&gt;

&lt;p&gt;Shader Graph 17.3 collects nodes upstream from active Master Stack blocks/output nodes. Unity's implementation is visible in &lt;a href="https://github.com/Unity-Technologies/Graphics/blob/6000.3/staging/Packages/com.unity.shadergraph/Editor/Generation/Processors/Generator.cs" rel="noopener noreferrer"&gt;Generator.cs&lt;/a&gt; and &lt;a href="https://github.com/Unity-Technologies/Graphics/blob/6000.3/staging/Packages/com.unity.shadergraph/Editor/Generation/Processors/GenerationUtils.cs" rel="noopener noreferrer"&gt;GenerationUtils.cs&lt;/a&gt;. A disconnected experiment therefore does not automatically run every frame.&lt;/p&gt;

&lt;p&gt;A connected Detail Map whose strength is &lt;code&gt;0&lt;/code&gt; at runtime is different: unless the compiler can prove the path unnecessary, the sample may remain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code is generated per pass and stage
&lt;/h3&gt;

&lt;p&gt;Targets/SubTargets define pass requirements. A simple-looking material may still participate in Forward, Depth, ShadowCaster, DepthNormals, MotionVectors, and other passes. Vertex deformation can therefore be repeated in shadow/depth passes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom Interpolators move suitable work to the vertex stage
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Custom-Interpolators.html" rel="noopener noreferrer"&gt;Custom Interpolators&lt;/a&gt; can pass vertex-calculated values to the fragment stage. A candidate must be vertex-valid, interpolation-safe, within the varying budget, and supported on every target. Shader Graph exposes up to &lt;strong&gt;32 channels, each float4-equivalent&lt;/strong&gt;, while real hardware/API limits can be lower. Check whether the value replaces an existing varying or merely adds another one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Precision is controllable
&lt;/h3&gt;

&lt;p&gt;Shader Graph supports Single and Half precision: &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Precision-Modes.html" rel="noopener noreferrer"&gt;Precision Modes&lt;/a&gt;. Start with Single for large ranges, accumulated time, and precision-sensitive UV math; test Half for normals, colors, directions, and limited-range masks. FP16 behavior varies by GPU/backend, so verify final code and image quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Branch node, static variants, and Dynamic Branch are different
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Branch node: both inputs are evaluated
&lt;/h3&gt;

&lt;p&gt;Unity's Branch node is effectively generated as:&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="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Unity_Branch_float4&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;Predicate&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;True&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;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;float4&lt;/span&gt; &lt;span class="n"&gt;Out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Predicate&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;True&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&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;p&gt;The &lt;code&gt;True&lt;/code&gt; and &lt;code&gt;False&lt;/code&gt; inputs are computed before the helper selects one. If both sides contain expensive samples or math, both sides remain work. See &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Branch-Node.html" rel="noopener noreferrer"&gt;Branch node&lt;/a&gt;. It is useful for selecting values, not reliably disabling an expensive feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shader Feature / Multi Compile: separate variants
&lt;/h3&gt;

&lt;p&gt;Static keywords generate separate variants. Inside a compiled variant, an inactive path can be constant-folded or removed. Keep this separate from &lt;strong&gt;build stripping&lt;/strong&gt;, which decides which variants survive into the Player.&lt;/p&gt;

&lt;p&gt;Shader Feature generally makes unused combinations easier to strip; Multi Compile retains combinations more aggressively. See &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/shader-conditionals-choose-a-type.html" rel="noopener noreferrer"&gt;Unity's branching guidance&lt;/a&gt; and the Shader Graph &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Keywords-reference.html" rel="noopener noreferrer"&gt;keyword reference&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic Branch: one runtime program
&lt;/h3&gt;

&lt;p&gt;A Shader Graph keyword defined as &lt;strong&gt;Dynamic Branch&lt;/strong&gt; produces one runtime program with a uniform branch; Unity sends its state as a uniform integer for the draw: &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Keywords-concepts.html" rel="noopener noreferrer"&gt;Keyword concepts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So the keyword itself is not normally a per-pixel predicate. Classic wave/warp divergence is more relevant to a &lt;strong&gt;varying branch in custom HLSL&lt;/strong&gt;, for example one driven by texture data. For Dynamic Branch keywords, measure branch overhead, both paths living in one program, register pressure, instruction-cache pressure, and path asymmetry.&lt;/p&gt;

&lt;p&gt;Do not choose Dynamic Branch only because a value changes often. Static variants can also be selected at runtime if the required variants survived stripping. Compare variant count, switching granularity, path cost, warm-up, memory, and GPU time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Shader Graph still does not optimize for you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate work:&lt;/strong&gt; repeated Sample Texture nodes are not guaranteed to collapse into one fetch. Reuse sampled results where possible; Sub Graphs are not memoization caches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expensive algorithms:&lt;/strong&gt; triplanar mapping, POM, procedural noise, Scene Color/Depth, transcendental math, extra lights, and shadows remain expensive when their underlying work is expensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overdraw:&lt;/strong&gt; transparency, full-screen effects, particles, foliage, and decals can remain fill-rate problems. Alpha Clip has different behavior from blending but still interacts with discard, early depth, MSAA, and dense overlap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline features:&lt;/strong&gt; shadows, fog, decals, SSAO, motion vectors, and pipeline textures are not automatically "Shader Graph overhead." A handwritten comparison is unfair if it simply removes those features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variant explosion:&lt;/strong&gt; ten independent two-way keywords already imply 1024 theoretical combinations before passes and pipeline keywords. This can hurt compile time, build size, loading, memory, and warm-up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think in three budgets: &lt;strong&gt;GPU execution&lt;/strong&gt;, &lt;strong&gt;CPU/render submission&lt;/strong&gt;, and &lt;strong&gt;build/load/variant cost&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Custom Function or handwritten HLSL is worth it
&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;Start with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same passes/stages/samples/precision and already within budget&lt;/td&gt;
&lt;td&gt;Keep Shader Graph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A local formula, loop, or special sample is awkward in nodes&lt;/td&gt;
&lt;td&gt;Custom Function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom passes, unusual buffers, stencil/render-state control dominate&lt;/td&gt;
&lt;td&gt;Handwritten ShaderLab/HLSL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The real issue is overdraw, pass count, lighting, or coverage&lt;/td&gt;
&lt;td&gt;Fix rendering design first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Custom Function is useful when most of the graph is productive but one algorithm needs explicit HLSL. Check the official &lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Custom-Function-Node.html" rel="noopener noreferrer"&gt;Custom Function Node&lt;/a&gt; rules for &lt;code&gt;$precision&lt;/code&gt;, &lt;code&gt;_half&lt;/code&gt;/&lt;code&gt;_float&lt;/code&gt; suffixes, include guards, texture wrapper types, and Shader Model/API/Target restrictions.&lt;/p&gt;

&lt;p&gt;Moving the same expression into a Custom Function does &lt;strong&gt;not&lt;/strong&gt; automatically make it faster. The gain comes when handwritten code enables a real algorithm or execution-structure improvement.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Count texture samples before nodes.&lt;/strong&gt; A graph with many arithmetic nodes can be cheap; dependent texture reads can dominate. Sample once and reuse where possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate affine from nonlinear vertex work.&lt;/strong&gt; &lt;code&gt;uv * tiling + offset&lt;/code&gt;, including uniform scrolling, is affine and can often move across interpolation without changing the result except for finite precision. &lt;code&gt;sin&lt;/code&gt;, noise, normalization, and distance-based distortion are nonlinear; compare sparse meshes and the lowest LOD.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose precision from numeric range.&lt;/strong&gt; Use Single for large ranges/accumulation; test Half for normals, colors, directions, and masks. Verify long runtimes and low-end targets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose branching for its trade-off.&lt;/strong&gt; Branch node = both inputs; Dynamic Branch = one uniform runtime program; Shader Feature/Multi Compile = variants; varying custom-HLSL branch = potential lane divergence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect every pass.&lt;/strong&gt; Disable unnecessary transparency, Alpha Clip, Two Sided, Receive Shadows, and related features. Remember that vertex work can repeat in ShadowCaster/depth passes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid giant keyword-driven uber shaders&lt;/strong&gt; when features barely overlap. Separate graphs or quality-specific graphs may be simpler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep CPU and GPU shader optimization separate.&lt;/strong&gt; The &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/SRPBatcher.html" rel="noopener noreferrer"&gt;SRP Batcher&lt;/a&gt; improves CPU setup/state changes; it does not merge draw calls like GPU Instancing, nor does it directly remove fragment instructions.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to compare Shader Graph and HLSL fairly
&lt;/h2&gt;

&lt;p&gt;Keep the rendering specification identical: pipeline, renderer, surface mode, passes, lights, textures/samplers, precision, keywords, mesh, camera, resolution, Render Scale, MSAA, and quality settings. If the handwritten shader drops a feature or pass, you are measuring a specification change.&lt;/p&gt;

&lt;p&gt;Use a Development Build for Unity Profiler and diagnostic logs. For final A/B decisions, use a &lt;strong&gt;product-like measurement build&lt;/strong&gt; matching the shipping Graphics API, Render Pipeline Asset, quality, resolution, IL2CPP configuration, and shader stripping, with only the hooks needed by your GPU tool.&lt;/p&gt;

&lt;p&gt;Unity 6.3's GPU Usage Profiler is unavailable or restricted on several configurations. Check the &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/ProfilerGPU.html" rel="noopener noreferrer"&gt;GPU Usage Profiler documentation&lt;/a&gt; and use RenderDoc, PIX, Xcode GPU tools, Android GPU Inspector, or platform-specific timing when appropriate. Frame Debugger is for draw/pass inspection, not GPU timing.&lt;/p&gt;

&lt;p&gt;Warm caches. Control VSync, Dynamic Resolution, frame caps, power state, background work, and device temperature. Measure A→B and B→A; record sample count, median, p95, and thermal state. If the difference is below the noise floor, treat it as no meaningful difference. Amplified tests are useful for diagnosis, but make the final decision in the real scene. Measure &lt;strong&gt;GPU milliseconds&lt;/strong&gt;, not only FPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect generated code and variant logs
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;View Generated Shader&lt;/code&gt; / &lt;code&gt;See Generated Code&lt;/code&gt; lets you check passes, stages, samples, keywords, precision, and varyings.&lt;/p&gt;

&lt;p&gt;For per-shader build information, search &lt;code&gt;Editor.log&lt;/code&gt; for &lt;code&gt;Compiling shader&lt;/code&gt;; Unity reports variant counts before and after stripping: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/shader-how-many-variants.html" rel="noopener noreferrer"&gt;shader variant counts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;URP aggregate stripping information in Unity 6.3&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Edit &amp;gt; Project Settings &amp;gt; Graphics&lt;/strong&gt; and select the &lt;strong&gt;URP&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Additional Shader Stripping Settings&lt;/strong&gt;, set &lt;strong&gt;Shader Variant Log Level&lt;/strong&gt; to anything except &lt;code&gt;Disabled&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Development Build&lt;/strong&gt; in &lt;strong&gt;File &amp;gt; Build Profiles&lt;/strong&gt; and build.&lt;/li&gt;
&lt;li&gt;Inspect the &lt;strong&gt;Shader Stripping&lt;/strong&gt; section in Console or &lt;code&gt;Editor.log&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With &lt;strong&gt;Export Shader Variants&lt;/strong&gt; enabled, Unity documents &lt;code&gt;Temp/graphics-settings-stripping.json&lt;/code&gt; and &lt;code&gt;Temp/shader-stripping.json&lt;/code&gt;: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/shader-stripping-check.html" rel="noopener noreferrer"&gt;URP shader stripping&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two tiny generated-code tests
&lt;/h2&gt;

&lt;p&gt;Use a fixed baseline such as &lt;strong&gt;Unity 6.3 LTS, Shader Graph 17.3, URP Unlit, Opaque, Alpha Clip off, Single precision&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disconnected sample
&lt;/h3&gt;

&lt;p&gt;Graph A samples one texture into Base Color. Graph B adds a second disconnected Sample Texture node. Search generated code for the second texture/sample reference. If it is absent, you have directly verified dependency-driven generation for that case. This does not prove that connected equivalent expressions are always merged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Branch versus keyword
&lt;/h3&gt;

&lt;p&gt;Create a heavy Detail path with an extra texture sample. Compare a Branch node, a Boolean Dynamic Branch keyword, and the same keyword as Shader Feature. Inspect branch/&lt;code&gt;#pragma&lt;/code&gt; structure and build variant counts; verify whether the static OFF variant removes the Detail sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical examples
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scrolling water UVs:&lt;/strong&gt; pure &lt;code&gt;uv * scale + offset&lt;/code&gt; is affine and can be a good vertex-stage candidate. If you also move noise, &lt;code&gt;sin&lt;/code&gt;, normalization, or distance-dependent distortion, compare the lowest LOD because those operations are nonlinear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detail Map on/off:&lt;/strong&gt; Branch can still evaluate Detail upstream; Dynamic Branch keeps one runtime program; Shader Feature can remove the OFF path but needs variant management; Multi Compile retains combinations more aggressively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full-screen procedural noise:&lt;/strong&gt; multiple noise octaves plus Scene Color distortion are expensive in both Shader Graph and HLSL. Try lower resolution, fewer octaves, smaller coverage, precomputed textures, LUTs, or approximations before rewriting. Handwritten HLSL matters when it enables real algorithmic changes such as early exits or specialized loops.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Build the same rendering specification in Shader Graph.&lt;/li&gt;
&lt;li&gt;Inspect passes, stages, texture samples, branches, varyings, and variants.&lt;/li&gt;
&lt;li&gt;Measure GPU time, image quality, loading, and stutter on the target device.&lt;/li&gt;
&lt;li&gt;Fix sample reuse, stage placement, coverage, passes, and keyword design first.&lt;/li&gt;
&lt;li&gt;Move only the remaining bottleneck to Custom Function or handwritten HLSL when the measured gain exceeds maintenance and portability cost.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For Unity 6.3 / Shader Graph 17.3, the useful shift from the old mindset is: &lt;strong&gt;Shader Graph can be the starting implementation; generated code and target-device profiling define the boundary where handwritten shader code becomes justified.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Release checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Product-like build: GPU time and image quality checked.&lt;/li&gt;
&lt;li&gt;[ ] A/B tests use identical rendering specifications and conditions.&lt;/li&gt;
&lt;li&gt;[ ] Caches warmed; median, p95, sample count, and thermal state recorded.&lt;/li&gt;
&lt;li&gt;[ ] Generated code checked for samples, stages, branches, varyings, and passes.&lt;/li&gt;
&lt;li&gt;[ ] Dynamic Branch keywords are not confused with varying per-pixel branches.&lt;/li&gt;
&lt;li&gt;[ ] Static variant counts, stripping, and required runtime combinations verified.&lt;/li&gt;
&lt;li&gt;[ ] Custom Interpolators checked for replacing versus adding varyings.&lt;/li&gt;
&lt;li&gt;[ ] Half/Single verified on the target backend/GPU and against image quality.&lt;/li&gt;
&lt;li&gt;[ ] Overdraw, Alpha Clip, lighting, and pipeline costs separated from authoring-format cost.&lt;/li&gt;
&lt;li&gt;[ ] Any HLSL rewrite produces a repeatable gain larger than noise and maintenance cost.&lt;/li&gt;
&lt;/ul&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/com.unity.shadergraph.html" rel="noopener noreferrer"&gt;Unity 6.3: Shader Graph package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Branch-Node.html" rel="noopener noreferrer"&gt;Shader Graph 17.3: Branch node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Keywords-concepts.html" rel="noopener noreferrer"&gt;Shader Graph 17.3: Keyword concepts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/shader-conditionals-choose-a-type.html" rel="noopener noreferrer"&gt;Unity 6.3: Shader branching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Custom-Interpolators.html" rel="noopener noreferrer"&gt;Shader Graph 17.3: Custom Interpolators&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.3/manual/Custom-Function-Node.html" rel="noopener noreferrer"&gt;Shader Graph 17.3: Custom Function Node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/ProfilerGPU.html" rel="noopener noreferrer"&gt;Unity 6.3: GPU Usage Profiler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/urp/shader-stripping-check.html" rel="noopener noreferrer"&gt;Unity 6.3: URP shader stripping&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/SRPBatcher.html" rel="noopener noreferrer"&gt;Unity 6.3: SRP Batcher&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>shadergraph</category>
      <category>graphics</category>
    </item>
    <item>
      <title>Choosing the Right Real-Time Networking Stack for Unity in 2026</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Fri, 04 Sep 2026 06:54:03 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/choosing-the-right-real-time-networking-stack-for-unity-in-2026-29f4</link>
      <guid>https://dev.to/gamedevtoollab/choosing-the-right-real-time-networking-stack-for-unity-in-2026-29f4</guid>
      <description>&lt;p&gt;When building an online game in Unity, the question is often framed as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Should I use Photon, Netcode for GameObjects, FishNet, or Mirror?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question is too small.&lt;/p&gt;

&lt;p&gt;In 2026, there is still no single networking product that is optimal for every Unity game. The real decision is a &lt;strong&gt;stack&lt;/strong&gt;: transport, netcode, authority model, session management, server hosting, and backend services.&lt;/p&gt;

&lt;p&gt;For a GameObject-based action game that needs client prediction, &lt;strong&gt;Photon Fusion 2.1 is a strong first PoC baseline&lt;/strong&gt;. If your priorities are Unity Gaming Services, DOTS/ECS, self-hosting, source access, or deterministic simulation, the starting point changes.&lt;/p&gt;

&lt;p&gt;This article explains how to make that decision in production terms: latency, cheating, reconnection, hosting, bandwidth, operations, and total cost.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This article uses official documentation checked on &lt;strong&gt;August 31, 2026&lt;/strong&gt; as its factual baseline. SDK versions, pricing, licensing, and service availability can change, so re-check them before committing a production project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What I mean by a real-time multiplayer game
&lt;/h2&gt;

&lt;p&gt;The target is roughly this class of game:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2–32 players in the same session&lt;/li&gt;
&lt;li&gt;continuously synchronized players, enemies, projectiles, or interactable objects&lt;/li&gt;
&lt;li&gt;input latency that directly affects game feel&lt;/li&gt;
&lt;li&gt;reconnects, host loss, and late joining that must be handled&lt;/li&gt;
&lt;li&gt;co-op action, FPS/TPS, racing, or competitive action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only need turn-based play, leaderboards, chat, friends, or asynchronous PvP, you may not need a sophisticated state-synchronization netcode at all. A backend such as Nakama, PlayFab, or Unity Gaming Services may be the more important part of the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not treat “networking” as one product
&lt;/h2&gt;

&lt;p&gt;A production multiplayer stack has at least five layers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;Packet delivery, reliability, connection path, secure channel integration&lt;/td&gt;
&lt;td&gt;Unity Transport, Photon transport, UDP/WebSocket-based transports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netcode&lt;/td&gt;
&lt;td&gt;Replication, RPCs, input, prediction, interpolation, rollback&lt;/td&gt;
&lt;td&gt;Fusion, NGO, Netcode for Entities, FishNet, Mirror&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session&lt;/td&gt;
&lt;td&gt;Lobby, matchmaking, relay, connection metadata&lt;/td&gt;
&lt;td&gt;Photon Realtime, MPS SDK, UGS Lobby/Relay/Matchmaker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Dedicated-server allocation, startup, scaling, monitoring&lt;/td&gt;
&lt;td&gt;GameLift, PlayFab MPS, Edgegap, Hathora, custom infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Authentication, persistence, inventory, ranking, result settlement&lt;/td&gt;
&lt;td&gt;PlayFab, Nakama, UGS, custom APIs and databases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is why “WebSocket or Photon?” is not a useful comparison. WebSocket is an application-layer protocol often used as a transport path in a game stack; Fusion is a netcode with prediction and simulation semantics.&lt;/p&gt;

&lt;p&gt;A transport can move packets. It does not automatically give you prediction, snapshot interpolation, authority rules, lag compensation, reconnect restoration, or late-join state reconstruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven criteria that matter more than brand names
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;What to decide&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prediction&lt;/td&gt;
&lt;td&gt;Can local input run ahead and be reconciled against authoritative state? How much is built in?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authority&lt;/td&gt;
&lt;td&gt;Who is allowed to decide movement, hits, rewards, inventory, and match results?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Topology&lt;/td&gt;
&lt;td&gt;Host, client-server, or distributed/shared authority?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replication scale&lt;/td&gt;
&lt;td&gt;Tick rate, visible objects, predicted physics, update frequency, interest management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platforms&lt;/td&gt;
&lt;td&gt;Mobile suspend/resume, WebGL constraints, console certification and invite flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team architecture&lt;/td&gt;
&lt;td&gt;GameObject or ECS? Do not migrate architecture just because the networking SDK prefers it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total cost&lt;/td&gt;
&lt;td&gt;CCU, bandwidth, hosting, observability, operations, migration and maintenance work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few terms used below: &lt;strong&gt;CCU&lt;/strong&gt; is concurrent users, &lt;strong&gt;RTT&lt;/strong&gt; is round-trip latency, &lt;strong&gt;SLO&lt;/strong&gt; is a service-level objective, &lt;strong&gt;egress&lt;/strong&gt; is outbound cloud traffic, and &lt;strong&gt;p95/p99&lt;/strong&gt; are percentile measurements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick the synchronization model before the SDK
&lt;/h2&gt;

&lt;p&gt;Most real-time games are built from three core synchronization models.&lt;/p&gt;

&lt;h3&gt;
  
  
  Snapshot replication
&lt;/h3&gt;

&lt;p&gt;The authority sends periodic state snapshots, and remote clients interpolate between them. This is a natural fit for remote players, NPCs, doors, and many world objects.&lt;/p&gt;

&lt;p&gt;The trade-off is bandwidth: higher update frequency improves responsiveness but increases traffic. Quantization, change detection, and interest management matter quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Input prediction and resimulation
&lt;/h3&gt;

&lt;p&gt;The local player applies input immediately, sends that input to the authority, then reconciles when authoritative state arrives. If needed, the client rewinds to the authoritative tick and resimulates to the present.&lt;/p&gt;

&lt;p&gt;This is a strong fit for FPS, TPS, racing, and action games, but it requires simulation code that can be run again safely. External I/O, analytics, rewards, and one-shot side effects must not be mixed into replayable tick logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deterministic lockstep with rollback
&lt;/h3&gt;

&lt;p&gt;Peers advance from the same input stream and rollback when delayed input arrives. This can be excellent for fighting games, RTS, sports, and other deterministic simulations, but it usually requires stricter simulation constraints than ordinary MonoBehaviour/PhysX gameplay.&lt;/p&gt;

&lt;p&gt;Real games are hybrids. Your local player may be predicted, remote players interpolated, cosmetic effects local-only, and economic rewards finalized by a backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  My 2026 decision table
&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 strength&lt;/th&gt;
&lt;th&gt;Main caution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Photon Fusion&lt;/td&gt;
&lt;td&gt;GameObject action, FPS/TPS, co-op&lt;/td&gt;
&lt;td&gt;Prediction/resimulation, lag compensation, multiple topologies&lt;/td&gt;
&lt;td&gt;Photon dependency, CCU/bandwidth cost, dedicated hosting is separate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NGO + MPS SDK&lt;/td&gt;
&lt;td&gt;Unity 6, UGS-heavy co-op&lt;/td&gt;
&lt;td&gt;Unity-first integration, Sessions/Relay/Lobby ecosystem&lt;/td&gt;
&lt;td&gt;MPS Sessions requires Unity 6; advanced action prediction is not a turnkey system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netcode for Entities&lt;/td&gt;
&lt;td&gt;DOTS/ECS, many entities, dedicated servers&lt;/td&gt;
&lt;td&gt;Ghosts, prediction, lag compensation&lt;/td&gt;
&lt;td&gt;ECS commitment, learning curve, resimulation cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FishNet&lt;/td&gt;
&lt;td&gt;GameObject, source access, self-hosting&lt;/td&gt;
&lt;td&gt;Prediction, server authority, transport flexibility&lt;/td&gt;
&lt;td&gt;Custom license; surrounding infrastructure and QA remain your responsibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mirror&lt;/td&gt;
&lt;td&gt;Existing codebases, MIT requirement, self-hosting&lt;/td&gt;
&lt;td&gt;Snapshot interpolation, lag compensation, PredictedRigidbody&lt;/td&gt;
&lt;td&gt;General prediction is still listed as Researching; PoC the exact physics/use case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Photon Quantum&lt;/td&gt;
&lt;td&gt;Deterministic competitive games&lt;/td&gt;
&lt;td&gt;Deterministic ECS and rollback&lt;/td&gt;
&lt;td&gt;Separate simulation model from ordinary Unity physics/MonoBehaviours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nakama&lt;/td&gt;
&lt;td&gt;Backend-driven multiplayer&lt;/td&gt;
&lt;td&gt;Auth, storage, matchmaking, chat, authoritative match loop&lt;/td&gt;
&lt;td&gt;Not a drop-in 3D action-state netcode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Versions I used as the baseline
&lt;/h3&gt;

&lt;p&gt;These are the specific versions/editor combinations checked for this article.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product / stack&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Editor / delivery note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Photon Fusion&lt;/td&gt;
&lt;td&gt;2.1.2 Stable&lt;/td&gt;
&lt;td&gt;Unity 2021.3.45, 2022.3.45, 6.0.x, 6.3.x; console access has separate requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netcode for GameObjects&lt;/td&gt;
&lt;td&gt;2.13.2&lt;/td&gt;
&lt;td&gt;Unity 6000.3; package manifest minimum is 6000.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiplayer Services SDK&lt;/td&gt;
&lt;td&gt;2.2.1&lt;/td&gt;
&lt;td&gt;Unity 6.0 LTS+; &lt;code&gt;com.unity.services.multiplayer&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netcode for Entities&lt;/td&gt;
&lt;td&gt;1.14.2&lt;/td&gt;
&lt;td&gt;Unity 6000.3 via UPM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netcode for Entities core&lt;/td&gt;
&lt;td&gt;6.5 / 6.6&lt;/td&gt;
&lt;td&gt;Integrated with Unity 6000.5 / 6000.6 editor lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FishNet&lt;/td&gt;
&lt;td&gt;4.7.2R&lt;/td&gt;
&lt;td&gt;Verify editor compatibility per release; custom source-available license&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mirror&lt;/td&gt;
&lt;td&gt;v96.11.2&lt;/td&gt;
&lt;td&gt;MIT; evaluate prediction maturity per feature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Photon Quantum&lt;/td&gt;
&lt;td&gt;3.0.13 Stable&lt;/td&gt;
&lt;td&gt;Unity 2021.3 LTS+; deterministic simulation environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nakama&lt;/td&gt;
&lt;td&gt;Server v3.40.0 / Unity Client 3.21.1&lt;/td&gt;
&lt;td&gt;Backend, not a transform-prediction netcode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For NGO and Netcode for Entities, do not confuse the broad product family with a specific package/editor combination. For example, NGO 2.13.2 is not something I would assume can simply be dropped into a Unity 2021/2022 project; older editors have their own package lines. Likewise, Netcode for Entities 6.5+ is tied to the corresponding Unity 6 editor line as a core package.&lt;/p&gt;

&lt;p&gt;Primary references: &lt;a href="https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.13/changelog/CHANGELOG.html" rel="noopener noreferrer"&gt;NGO 2.13 changelog&lt;/a&gt;, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/com.unity.netcode.gameobjects.html" rel="noopener noreferrer"&gt;Unity 6000.3 NGO&lt;/a&gt;, &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/com.unity.netcode.html" rel="noopener noreferrer"&gt;Unity 6000.3 Netcode for Entities&lt;/a&gt;, and the &lt;a href="https://unity.com/releases/editor/beta/6000.6.0b9" rel="noopener noreferrer"&gt;Unity 6000.6.0b9 release page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Fusion is a sensible first benchmark
&lt;/h2&gt;

&lt;p&gt;I would start a GameObject action-game PoC with Fusion when all of these are acceptable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;movement, shooting, or physics needs client prediction;&lt;/li&gt;
&lt;li&gt;Photon Cloud dependency and its pricing model are acceptable;&lt;/li&gt;
&lt;li&gt;Host or client-server topology matches the product;&lt;/li&gt;
&lt;li&gt;full source redistribution or unrestricted middleware reuse is not a hard requirement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a &lt;strong&gt;comparison baseline, not an automatic production choice&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Fusion Host Mode is attractive for small PvE games because it can avoid dedicated-server compute cost. Server Mode is a better fit when match integrity matters. Shared Mode can reduce some topology friction for casual/mobile/WebGL use cases, but its authority model must not be treated as equivalent to a trusted dedicated server.&lt;/p&gt;

&lt;p&gt;Photon provides Fusion Server Mode and the connection/session runtime. It does &lt;strong&gt;not&lt;/strong&gt; automatically provide the infrastructure that builds, allocates, starts, scales, and monitors your Unity headless server fleet. That is a hosting/orchestration problem. See Photon’s &lt;a href="https://doc.photonengine.com/fusion/v2/concepts-and-patterns/dedicated-server-overview" rel="noopener noreferrer"&gt;Dedicated Server overview&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Console development also has separate access requirements. Photon’s &lt;a href="https://doc.photonengine.com/fusion/v2/consoles/overview" rel="noopener noreferrer"&gt;Console Development Overview&lt;/a&gt; describes certification checks, platform-specific documentation, and native socket libraries. Authentication, invites, suspend/resume, cross-play, and platform certification still need device-level PoCs.&lt;/p&gt;

&lt;h2&gt;
  
  
  NGO + MPS SDK: best when Unity 6 and UGS are deliberate choices
&lt;/h2&gt;

&lt;p&gt;Netcode for GameObjects is Unity’s GameObject/MonoBehaviour-oriented netcode. My current baseline is &lt;strong&gt;Unity 6000.3 + NGO 2.13.2&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.unity.com/en-us/mps-sdk/sessions" rel="noopener noreferrer"&gt;Multiplayer Services SDK Sessions&lt;/a&gt; requires Unity 6.0 LTS or newer. Sessions can automate host election, player join/leave handling, and network connection establishment. That does not mean it designs your gameplay state, authority model, session lifetime, migration payloads, or failure UX.&lt;/p&gt;

&lt;p&gt;For existing Unity 2021/2022 projects, treat “stay on the editor-compatible NGO package” and “migrate to Unity 6 + current NGO + MPS Sessions” as separate estimates.&lt;/p&gt;

&lt;p&gt;NGO can be a very good fit for small co-op games where UGS Authentication, Lobby, Relay, and Matchmaker are already part of the product. For highly latency-sensitive competitive action, I would PoC the actual movement and combat loop under 100–200 ms rather than assume anticipation features are equivalent to a complete rollback/resimulation architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Netcode for Entities: use it when ECS is already the product architecture
&lt;/h2&gt;

&lt;p&gt;Netcode for Entities provides Ghost snapshots, command streams, client prediction, rollback/resimulation, interpolation, and lag compensation in an ECS-oriented execution model.&lt;/p&gt;

&lt;p&gt;Its strength is not “more players by magic.” You still need to control which ghosts are predicted, how often they update, and which clients receive them. Sending every entity every tick to every client will fail regardless of the framework.&lt;/p&gt;

&lt;p&gt;I would choose it when DOTS/ECS and dedicated servers are already strategic decisions: large numbers of players, NPCs, projectiles, or units; data-oriented simulation; and a team that can profile prediction loops and server tick budgets.&lt;/p&gt;

&lt;p&gt;I would not move a mature MonoBehaviour/Animator/PhysX/NavMesh project to ECS solely because the networking package looks attractive.&lt;/p&gt;

&lt;h2&gt;
  
  
  FishNet: strong for self-hosted GameObject stacks, but read the license correctly
&lt;/h2&gt;

&lt;p&gt;FishNet offers server authority, prediction/reconciliation, NetworkTransform, observer systems, and transport choice. It can work well when you want to choose your own hosting and surrounding services.&lt;/p&gt;

&lt;p&gt;However, do not describe it simply as “open source.” The current &lt;a href="https://raw.githubusercontent.com/FirstGearGames/FishNet/main/LICENSE.md" rel="noopener noreferrer"&gt;FishNet license&lt;/a&gt; permits game/content use and modification, while section 2.b restricts other networking-solution products from using, reverse-engineering, or implementing the FishNet software. Section 2.d explicitly says that exclusion does not apply to games developed with the software.&lt;/p&gt;

&lt;p&gt;So separate normal game development from middleware/product reuse. If your plan includes forking, redistribution, an internal shared networking platform, a competing networking product, or FishNet-Pro distribution, get legal review of the current terms.&lt;/p&gt;

&lt;p&gt;Self-hosting also means you own more of relay/NAT strategy, allocation, monitoring, DDoS considerations, upgrades, and regression testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mirror: evaluate the current features, not the old reputation
&lt;/h2&gt;

&lt;p&gt;Mirror remains relevant when you value an MIT license, existing assets, and self-hosting.&lt;/p&gt;

&lt;p&gt;It has &lt;a href="https://mirror-networking.gitbook.io/docs/manual/components/network-transform/snapshot-interpolation" rel="noopener noreferrer"&gt;Snapshot Interpolation&lt;/a&gt;, &lt;a href="https://mirror-networking.gitbook.io/docs/manual/general/lag-compensation" rel="noopener noreferrer"&gt;Lag Compensation&lt;/a&gt;, and &lt;a href="https://mirror-networking.gitbook.io/docs/manual/general/client-side-prediction" rel="noopener noreferrer"&gt;PredictedRigidbody&lt;/a&gt;. But the official project status still distinguishes feature maturity: snapshot interpolation is Stable, lag compensation is Beta, and general Prediction is Researching.&lt;/p&gt;

&lt;p&gt;That does not make Mirror unsuitable for action games. It means the PoC must use the exact Unity version, transport, physics objects, character controller, abilities, knockback, and reconciliation requirements you expect to ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quantum: deterministic does not mean “automatically cheat-proof”
&lt;/h2&gt;

&lt;p&gt;Photon Quantum uses deterministic ECS and rollback. That can be a very good fit for fighting games, RTS, sports, and other simulations where replayability from input history is valuable.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;determinism is not the same as dedicated-server authority&lt;/strong&gt;. Hidden information, fog of war, input validity, ranking, and reward settlement are separate security problems. Use the official &lt;a href="https://doc.photonengine.com/quantum/current/manual/cheat-protection" rel="noopener noreferrer"&gt;Quantum Cheat Protection&lt;/a&gt; guidance to decide what is validated, what checksums can detect, and what must be verified or persisted elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nakama: backend first, not a replacement for action netcode
&lt;/h2&gt;

&lt;p&gt;Nakama provides authentication, storage, matchmaking, chat, leaderboards, realtime sockets, and authoritative match handlers. It can fully own many card, board, or lower-frequency competitive games.&lt;/p&gt;

&lt;p&gt;It is not, by itself, a drop-in replacement for a 3D action framework that predicts CharacterControllers, Rigidbodies, projectiles, and hitboxes. For a 60 Hz action game, pair it with a netcode or build a game-specific protocol deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unity Multiplay changed: separate matchmaking from hosting
&lt;/h2&gt;

&lt;p&gt;Unity’s directly provided Multiplay Game Server Hosting is marked deprecated as of April 1, 2026 in the &lt;a href="https://status.unity.com/info_notices/362941" rel="noopener noreferrer"&gt;Unity status notice&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That does &lt;strong&gt;not&lt;/strong&gt; mean Matchmaker or the Multiplayer Services SDK disappeared. Unity’s &lt;a href="https://docs.unity.com/en-us/mps-sdk/game-server-hosting-support" rel="noopener noreferrer"&gt;game server hosting support&lt;/a&gt; documents an architecture where Cloud Code integrates with an external game-server hosting provider for allocation, session creation, and backfill.&lt;/p&gt;

&lt;p&gt;A new design should therefore keep these responsibilities separate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication
   ↓
Matchmaker
   ↓
Cloud Code / allocation logic
   ↓
External game-server hosting
   ↓
MPS Session
   ↓
Netcode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same principle applies to Fusion: netcode is not hosting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Host Mode: the backend cannot magically make host-reported gameplay trustworthy
&lt;/h2&gt;

&lt;p&gt;For small co-op games, a common design is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Player-hosted match
   ↓
Backend
   ├─ authentication
   ├─ server-issued mission/reward rules
   ├─ eligibility and limit validation
   └─ capped provisional reward commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can be useful, but the participant acting as host owns State Authority and can modify the host process. Sending results to a backend does not prove that reported kills, drops, or clears actually happened.&lt;/p&gt;

&lt;p&gt;Server-issued mission tokens, time windows, reward tables, and daily caps can verify &lt;strong&gt;eligibility&lt;/strong&gt; and limit the maximum loss from cheating. They do not prove host-reported gameplay facts inside those limits.&lt;/p&gt;

&lt;p&gt;If high-value rewards, ranking, or paid currency require stronger trust, move those facts behind a trusted authority: for example dedicated-server telemetry or another verifiable event source outside the player host’s trust boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send verifiable input or commands, not authoritative results
&lt;/h2&gt;

&lt;p&gt;A client should not be trusted when it says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“my position is X”&lt;/li&gt;
&lt;li&gt;“I dealt 100 damage”&lt;/li&gt;
&lt;li&gt;“this purchase succeeded”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an action game, send movement, aim, buttons, and fire requests. The authority derives speed, cooldowns, ammo, collisions, and damage.&lt;/p&gt;

&lt;p&gt;For a card or board game, raw device input is less useful. Send intent-level commands such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PlayCard(cardId, targetId)
UseSkill(skillId, targetId)
BuyItem(sku, quantity)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The authority validates ownership, turn/phase, cost, target, inventory, cooldown, and sequence, then derives the result.&lt;/p&gt;

&lt;p&gt;The common principle is simple: &lt;strong&gt;the client should submit the minimum input/command that a trusted authority can validate and recompute.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep tick simulation replayable
&lt;/h2&gt;

&lt;p&gt;For client-server prediction, gameplay code that affects authoritative results belongs in network ticks, not only in ordinary &lt;code&gt;Update()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;conceptual Fusion Host/Server Mode example&lt;/strong&gt;, not copy-paste production 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;struct&lt;/span&gt; &lt;span class="nc"&gt;PlayerMoveInput&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;INetworkInput&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="n"&gt;Move&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;NetworkPlayer&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NetworkBehaviour&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;byte&lt;/span&gt; &lt;span class="n"&gt;MaxMoveReuseTicks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Networked&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Networked&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;NetworkBool&lt;/span&gt; &lt;span class="n"&gt;IsStunned&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Networked&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;Vector2&lt;/span&gt; &lt;span class="n"&gt;LastAcceptedMove&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Networked&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;byte&lt;/span&gt; &lt;span class="n"&gt;MissingMoveTicks&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="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;FixedUpdateNetwork&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;HasInputAuthority&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;HasStateAuthority&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="c1"&gt;// State that advances without player input must still tick.&lt;/span&gt;
        &lt;span class="nf"&gt;SimulateInputIndependentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Runner&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;Vector2&lt;/span&gt; &lt;span class="n"&gt;move&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;GetInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;PlayerMoveInput&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;move&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ClampMagnitude&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;Move&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;LastAcceptedMove&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;MissingMoveTicks&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="k"&gt;else&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;MissingMoveTicks&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;MissingMoveTicks&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;MaxMoveReuseTicks&lt;/span&gt;
                &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;LastAcceptedMove&lt;/span&gt;
                &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Vector2&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;&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="n"&gt;IsStunned&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;Vector2&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="nf"&gt;SimulateMovement&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;Runner&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 important part is not the exact two-tick policy. It is that the policy is deterministic and explicit. Photon documents that &lt;code&gt;GetInput()&lt;/code&gt; can occasionally fail even on the State Authority during packet loss or latency spikes.&lt;/p&gt;

&lt;p&gt;Held input and edge input should not necessarily use the same missing-packet policy. Reusing a previous held movement state for a bounded number of ticks can be reasonable; inventing a new button-down edge from missing input is not.&lt;/p&gt;

&lt;p&gt;Also keep external side effects out of replayable simulation. Do not call databases, HTTP services, anti-cheat services, analytics, or reward settlement directly from resimulatable tick code.&lt;/p&gt;

&lt;p&gt;For economic or external effects, use a durable, retry-safe flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;confirmed authoritative domain event
   ↓
durable event log / transactional outbox
   ↓  at-least-once delivery
worker / dispatcher
   ↓
backend with persistent idempotency key
   ↓
persisted result / retryable query
   ↓
apply only if match/player/revision is still valid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An “outbox” stored only in game-server memory or disposable local disk is not crash-safe. If the match state and outbox cannot be committed atomically, you still have a dual-write problem. Either make the durable event the source of truth, use a transactional boundary, or define how the match is invalidated/compensated after unrecoverable failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design session lifetime and reconnection before polishing gameplay
&lt;/h2&gt;

&lt;p&gt;A connection API returning success is not a session design.&lt;/p&gt;

&lt;p&gt;Define the state transitions for create, join, ready, start, late join, temporary disconnect, reconnect, leave, result settlement, and destruction. Decide which record is authoritative when lobby, game server, and backend disagree.&lt;/p&gt;

&lt;p&gt;Do not use a transient connection ID as the persistent player identity. Reconnect using an authenticated user identity plus match identity, with an expiring token that includes build/version and permissions. Invalidate the old connection when the new one is accepted.&lt;/p&gt;

&lt;p&gt;Late joiners should receive scene/assets, current phase, and replicated state in a known order, then acknowledge readiness before spawning and accepting input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run a 1–2 week PoC with at most two candidates
&lt;/h2&gt;

&lt;p&gt;Do not choose a multiplayer SDK from documentation alone.&lt;/p&gt;

&lt;p&gt;A useful first timebox is one or two engineers, one to two weeks, and no more than two candidates. Build the same minimum slice in each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;real movement plus one important action such as dash/jump/dodge&lt;/li&gt;
&lt;li&gt;shooting or melee, damage, death&lt;/li&gt;
&lt;li&gt;one moving platform or pushable Rigidbody&lt;/li&gt;
&lt;li&gt;match start/end, late join, disconnect, reconnect&lt;/li&gt;
&lt;li&gt;headless build where relevant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the same gameplay and network conditions for every candidate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define pass/fail thresholds before testing
&lt;/h3&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Example acceptance rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reconciliation&lt;/td&gt;
&lt;td&gt;At 100 ms RTT and 3% loss, p95 correction distance stays below your game-specific limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server tick&lt;/td&gt;
&lt;td&gt;p99 tick time remains below 70% of the tick budget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconnect&lt;/td&gt;
&lt;td&gt;p95 reconnect completes within the product target and restores authoritative state correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bandwidth&lt;/td&gt;
&lt;td&gt;Average billable bytes/sec per connection stays inside the business target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allocation&lt;/td&gt;
&lt;td&gt;Dedicated-server allocation success and cold-start p95 meet the SLO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure handling&lt;/td&gt;
&lt;td&gt;Expired tokens, version mismatch, and region exhaustion fail explicitly and recover predictably&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result settlement&lt;/td&gt;
&lt;td&gt;Crashes and duplicate submissions do not create missing or double-applied match results&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not copy arbitrary values like “0.2 m correction” from another game. Derive thresholds from movement speed, match length, reconnect rules, platform constraints, and economic risk.&lt;/p&gt;

&lt;p&gt;Network emulation should include not only RTT and random packet loss, but also jitter, burst loss, asymmetric uplink/downlink, packet reordering/duplication, temporary disconnects, host loss, backgrounding, different frame rates, and server tick stalls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost: estimate from average CCU and billable traffic, not DAU alone
&lt;/h2&gt;

&lt;p&gt;A rough average CCU 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;average CCU
  ≈ DAU × sessions per day × average connected minutes / 1,440
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, 10,000 DAU × 1.5 sessions × 20 minutes is roughly 208 average CCU.&lt;/p&gt;

&lt;p&gt;Bandwidth is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;monthly billable transfer
  ≈ average CCU × average billable bytes/sec per connection × seconds/month
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;strong&gt;peak CCU&lt;/strong&gt; for capacity and server-count planning, but average usage for transfer estimates.&lt;/p&gt;

&lt;p&gt;Then price each network leg separately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;client → relay and relay → host/server&lt;/li&gt;
&lt;li&gt;host/server → client&lt;/li&gt;
&lt;li&gt;game server → backend&lt;/li&gt;
&lt;li&gt;cross-region and redundancy traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same packet may be billable by multiple providers, while some hosting plans include certain bandwidth. Only remove a cost after verifying the provider’s pricing model. Relevant references include &lt;a href="https://unity.com/products/gaming-services/pricing" rel="noopener noreferrer"&gt;UGS pricing&lt;/a&gt;, &lt;a href="https://www.photonengine.com/fusion/pricing" rel="noopener noreferrer"&gt;Photon Fusion pricing&lt;/a&gt;, and &lt;a href="https://aws.amazon.com/gamelift/servers/pricing/" rel="noopener noreferrer"&gt;Amazon GameLift Servers pricing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical recommendation
&lt;/h2&gt;

&lt;p&gt;In 2026, I would narrow a Unity real-time networking stack in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;decide GameObject vs ECS and the Unity/editor version you can actually ship;&lt;/li&gt;
&lt;li&gt;choose where authority lives: host, dedicated client-server, or distributed/shared;&lt;/li&gt;
&lt;li&gt;separate session, hosting, and backend responsibilities from netcode;&lt;/li&gt;
&lt;li&gt;validate platform access, licensing, CCU/bandwidth, and operational cost;&lt;/li&gt;
&lt;li&gt;compare the real movement/combat/reconnect slice under the same bad-network conditions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the game is GameObject-based, you accept Photon dependency and pricing, and you need Host or Client-Server prediction, &lt;strong&gt;Fusion 2.1 is a sensible benchmark candidate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If Unity 6 + UGS integration is the priority, start with NGO 2.13.2 + MPS SDK. If DOTS/ECS is already the architecture, start with Netcode for Entities. If source access and self-hosting matter, compare FishNet and Mirror carefully under their actual licensing and feature-maturity constraints. If determinism is a core requirement, evaluate Quantum. Use Nakama primarily as a backend or authoritative match platform rather than as a drop-in transform prediction layer.&lt;/p&gt;

&lt;p&gt;The key point is that &lt;strong&gt;“it connected” is not the success condition&lt;/strong&gt;. Ship only after you have measurable acceptance criteria for 100–200 ms network conditions, reconciliation, authority and cheating, reconnect/late join, server allocation, bandwidth, and crash-safe result settlement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Primary official references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://doc.photonengine.com/fusion/v2/getting-started/sdk-download" rel="noopener noreferrer"&gt;Photon Fusion SDK Download&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.photonengine.com/fusion/v2/fusion-choose" rel="noopener noreferrer"&gt;Photon Fusion topology guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.photonengine.com/fusion/v2/manual/input/player-input" rel="noopener noreferrer"&gt;Photon Fusion input&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.photonengine.com/fusion/v2/concepts-and-patterns/network-simulation-loop" rel="noopener noreferrer"&gt;Photon Fusion network simulation loop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.13/changelog/CHANGELOG.html" rel="noopener noreferrer"&gt;Unity NGO 2.13 changelog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity.com/en-us/mps-sdk/sessions" rel="noopener noreferrer"&gt;Unity MPS SDK Sessions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://status.unity.com/info_notices/362941" rel="noopener noreferrer"&gt;Unity Multiplay deprecation notice&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.netcode@1.14/changelog/CHANGELOG.html" rel="noopener noreferrer"&gt;Unity Netcode for Entities 1.14 changelog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/FirstGearGames/FishNet/main/LICENSE.md" rel="noopener noreferrer"&gt;FishNet license&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/MirrorNetworking/Mirror" rel="noopener noreferrer"&gt;Mirror README / feature status&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.photonengine.com/quantum/current/manual/cheat-protection" rel="noopener noreferrer"&gt;Photon Quantum cheat protection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://heroiclabs.com/docs/nakama/concepts/multiplayer/authoritative/" rel="noopener noreferrer"&gt;Nakama authoritative multiplayer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>multiplayer</category>
      <category>networking</category>
    </item>
    <item>
      <title>Will HTTP/3 Make Unity Faster? Choosing HTTP for APIs, Asset Downloads, and Real-Time Networking</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Wed, 02 Sep 2026 06:19:56 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/will-http3-make-unity-faster-choosing-http-for-apis-asset-downloads-and-real-time-networking-4lgp</link>
      <guid>https://dev.to/gamedevtoollab/will-http3-make-unity-faster-choosing-http-for-apis-asset-downloads-and-real-time-networking-4lgp</guid>
      <description>&lt;p&gt;Unity networking can look simple: call &lt;code&gt;UnityWebRequest.Get()&lt;/code&gt;, wait, and parse. In production, protocol choice also affects concurrent APIs, Addressables, mobile packet loss, platform backends, CDNs, and real-time communication.&lt;/p&gt;

&lt;p&gt;The practical default is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Use normal protocol negotiation and treat HTTP/2 as the baseline. Evaluate HTTP/3 only where real-device measurements justify its integration and maintenance cost.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;HTTP/3 is not automatically faster, and it does not determine action-game synchronization. This article separates APIs, asset delivery, and real-time networking.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Scope: Unity 6.3 through Unity 6.5. No benchmark numbers are claimed. Behavior depends on the Unity version, platform, package versions, server, and CDN. Compile against your actual package set and verify the negotiated protocol on real devices.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Recommendations by workload
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Start with&lt;/th&gt;
&lt;th&gt;Consider HTTP/3 when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Login, inventory, billing, and master-data APIs&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;UnityWebRequest&lt;/code&gt; with normal negotiation; HTTP/2 where available&lt;/td&gt;
&lt;td&gt;Many independent calls run over lossy mobile links and measurements show a gain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple small or medium assets&lt;/td&gt;
&lt;td&gt;HTTP/2, CDN caching, sensible bundle layout, and bounded concurrency&lt;/td&gt;
&lt;td&gt;Packet loss stalls unrelated parallel downloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One large patch&lt;/td&gt;
&lt;td&gt;HTTP/2 plus Range requests, resume logic, and integrity checks&lt;/td&gt;
&lt;td&gt;Reconnects or network changes improve measurably with QUIC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background download&lt;/td&gt;
&lt;td&gt;Native OS download APIs through a plugin&lt;/td&gt;
&lt;td&gt;Background execution and reliable resume justify native integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time state, chat, or presence&lt;/td&gt;
&lt;td&gt;Photon, Unity Transport, WebSocket, or gRPC as appropriate&lt;/td&gt;
&lt;td&gt;Evaluate separately from HTTP REST&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not hard-code one version everywhere. Normal negotiation allows HTTP/2 with HTTP/1.1 fallback for incompatible devices, proxies, or networks.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTTP/1.1, HTTP/2, and HTTP/3
&lt;/h2&gt;

&lt;h3&gt;
  
  
  HTTP/1.1: compatible, but awkward for many parallel requests
&lt;/h3&gt;

&lt;p&gt;HTTP/1.1 runs over TCP and reuses connections with keep-alive, but cannot flexibly multiplex independent responses. Clients therefore open several connections to one host, which matters for many startup APIs or small bundles.&lt;/p&gt;

&lt;p&gt;It remains sufficient for a few calls, server-dominated workloads, or one stable large transfer. Its main value is compatibility and rollback.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP/2: the current baseline
&lt;/h3&gt;

&lt;p&gt;HTTP/2 also uses TCP, but multiplexes streams in one connection. HPACK reduces repeated headers, helping APIs with similar authorization and cookies. Unity benefits from fewer TCP connections and efficient multi-file CDN delivery.&lt;/p&gt;

&lt;p&gt;HTTP/2 still inherits TCP-level head-of-line blocking. If a TCP packet is lost, bytes after the gap wait for retransmission, so unrelated streams on that connection can pause.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP/3: QUIC reduces interference between streams
&lt;/h3&gt;

&lt;p&gt;HTTP/3 uses QUIC over UDP, integrates TLS 1.3, and provides independent streams. It can reduce cross-stream blocking, shorten setup, and better tolerate Wi-Fi-to-cellular transitions—especially on lossy mobile links.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.rfc-editor.org/rfc/rfc9114.html" rel="noopener noreferrer"&gt;HTTP/3 specification&lt;/a&gt; explains how independent QUIC streams avoid the TCP-level cross-stream blocking that can affect HTTP/2.&lt;/p&gt;

&lt;p&gt;Three caveats matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One huge file is not automatically faster.&lt;/strong&gt; It normally uses one stream, where loss still requires retransmission. The advantage is clearer when catalogs, bundles, and APIs run together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UDP/443 is not universal.&lt;/strong&gt; Networks may restrict it, so HTTP/2 or HTTP/1.1 fallback is mandatory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0-RTT has replay implications.&lt;/strong&gt; Do not blindly resend purchases, currency spending, or loot-box draws. Use operation IDs, idempotency keys, deduplication, and status queries.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/3 over QUIC
    ↓ unavailable or unsuitable
HTTP/2 over TLS/TCP
    ↓ unavailable
HTTP/1.1 over TLS/TCP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a conceptual relationship. An implementation may race candidates rather than wait for every failure serially.&lt;/p&gt;

&lt;h2&gt;
  
  
  What version does Unity actually use?
&lt;/h2&gt;

&lt;p&gt;Specifications are only half the answer. Unity's effective backend changes by editor version and platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unity 6.3: HTTP/2 by default on supported platforms
&lt;/h3&gt;

&lt;p&gt;Unity's &lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/WhatsNewUnity63.html" rel="noopener noreferrer"&gt;6.3 documentation&lt;/a&gt; says &lt;code&gt;UnityWebRequest&lt;/code&gt; uses HTTP/2 by default on supported platforms, listing Android, Embedded Linux, Linux, macOS, PS4, PS5, UWP, and Windows.&lt;/p&gt;

&lt;p&gt;That list does not prove that every unlisted platform lacks HTTP/2. iOS and web builds need separate verification.&lt;/p&gt;

&lt;p&gt;Android also exposes a rollback switch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project Settings &amp;gt; Player &amp;gt; Android &amp;gt; Other Settings &amp;gt; Configuration
Force UnityWebRequest via HTTP/1.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it for diagnosis or compatibility, not as the normal default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unity 6.5: forcing HTTP/1.1 or HTTP/2
&lt;/h3&gt;

&lt;p&gt;Unity 6.5 adds &lt;code&gt;UnityWebRequest.httpForcedVersion&lt;/code&gt;. Its default is &lt;code&gt;HttpForcedVersion.NotForced&lt;/code&gt;, which leaves selection to negotiation. The available enum values stop at HTTP/2; there is no HTTP/3 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="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine.Networking&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;ApiClient&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;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;GetJson&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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;long&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;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UnityWebRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetRequestHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Accept"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="cp"&gt;#if UNITY_6000_5_OR_NEWER
&lt;/span&gt;        &lt;span class="c1"&gt;// Already the default; explicit here to show the intended policy.&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;httpForcedVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HttpForcedVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotForced&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendWebRequest&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;request&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;UnityWebRequest&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;Success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;downloadHandler&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="k"&gt;else&lt;/span&gt;
            &lt;span class="n"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;responseCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&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="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 demonstrates version policy, not a production client. Production code still needs cancellation, deadlines, bounded retries, status handling, size limits, authentication refresh, redirects, request IDs, and idempotency.&lt;/p&gt;

&lt;p&gt;Force HTTP/1.1 or HTTP/2 only for comparison or rollback; permanent forcing can disable fallback. Unity's &lt;a href="https://unity.com/releases/editor/whats-new/6000.5.0f1" rel="noopener noreferrer"&gt;6.5 release notes&lt;/a&gt; include an HTTP/2 fix for POST bodies above 600 KB, so test the exact editor patch you ship.&lt;/p&gt;

&lt;p&gt;Unity 6.5 also adds &lt;code&gt;UnityHttpMessageHandler&lt;/code&gt; for &lt;code&gt;HttpClient&lt;/code&gt; and gRPC. It neither enables HTTP/3 automatically nor makes gRPC an action-game transport; test the exact IL2CPP, AOT, generated-code, and package combination.&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS: separate client path, TLS, and HTTP/3 capability
&lt;/h3&gt;

&lt;p&gt;URLSession on iOS 15 and later supports HTTP/3 and discovers support through &lt;code&gt;Alt-Svc&lt;/code&gt; or DNS HTTPS resource records. That alone does not prove every Unity request uses URLSession.&lt;/p&gt;

&lt;p&gt;For Unity 6.5, keep three facts separate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The release notes state that iOS &lt;code&gt;UnityWebRequest&lt;/code&gt; moved from &lt;code&gt;NSURLSession&lt;/code&gt; to &lt;code&gt;libcurl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Mbed TLS concerns TLS processing; it is not the HTTP client replacing URLSession.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HttpForcedVersion&lt;/code&gt; has no HTTP/3 enum, so verify negotiation rather than infer it from iOS support.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Verify ordinary &lt;code&gt;UnityWebRequest&lt;/code&gt; through CDN/server logs, ALPN, or platform instrumentation. For URLSession background transfer, resume integration, or an intentional HTTP/3 path, use a Swift or Objective-C plugin. See Apple's &lt;a href="https://developer.apple.com/documentation/technotes/tn3102-http3-in-your-app" rel="noopener noreferrer"&gt;HTTP/3 in your app&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unity Web: the browser chooses
&lt;/h3&gt;

&lt;p&gt;Unity web builds route &lt;code&gt;UnityWebRequest&lt;/code&gt; through the Fetch API. The application cannot directly open arbitrary TCP or UDP sockets, so the browser and hosting environment choose HTTP/2 or HTTP/3.&lt;/p&gt;

&lt;p&gt;Verify CDN/origin support, CORS headers, the browser developer tools' Protocol column, and browser support for WebSocket, WebRTC, or WebTransport. Unity's &lt;a href="https://docs.unity3d.com/6000.5/Documentation/Manual/webgl-networking.html" rel="noopener noreferrer"&gt;web networking documentation&lt;/a&gt; describes the Fetch and CORS constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workload 1: ordinary game APIs
&lt;/h2&gt;

&lt;p&gt;A slow login call may spend most of its time in the server. Measure the entire path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DNS + connection + TLS + request upload + server wait
+ response download + decompression/JSON + main-thread application
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTTP/2 and HTTP/3 can improve connection reuse, concurrency, and loss behavior. They do not fix slow database queries, billing calls, lock contention, oversized JSON, or expensive deserialization.&lt;/p&gt;

&lt;p&gt;A practical optimization order is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;measure server time per endpoint;&lt;/li&gt;
&lt;li&gt;remove unnecessary startup dependencies and merge calls where sensible;&lt;/li&gt;
&lt;li&gt;reduce redundant fields and headers;&lt;/li&gt;
&lt;li&gt;use suitable compression;&lt;/li&gt;
&lt;li&gt;verify HTTP/2 negotiation and connection reuse;&lt;/li&gt;
&lt;li&gt;evaluate HTTP/3 only if mobile-network problems remain.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Multiplexing does not mean “send everything at once.” Group calls as boot-critical, first-screen, or deferrable, and shorten the critical path without overloading the server.&lt;/p&gt;

&lt;p&gt;Retry according to semantics. A failed GET can often use bounded exponential backoff. Blindly retrying a purchase or currency spend can execute it twice. Idempotency remains an application concern under every HTTP version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workload 2: Addressables and asset delivery
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fix delivery design before adding HTTP/3
&lt;/h3&gt;

&lt;p&gt;Slow Addressables downloads can come from too many tiny bundles, oversized bundles, duplicated dependencies, a distant CDN, broken cache keys, unsuitable compression, excessive concurrency, or weak catalog consistency.&lt;/p&gt;

&lt;p&gt;HTTP/3 does not repair hundreds of tiny bundles or a URL scheme that always misses cache. First make bundle layout and CDN delivery work well over HTTP/2, then compare HTTP/3 under realistic packet loss.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bound concurrency even with HTTP/2
&lt;/h3&gt;

&lt;p&gt;Multiplexing does not remove receive buffers, decryption, decompression, storage writes, hash checks, or main-thread spikes when many downloads finish together. CDNs also limit concurrent streams.&lt;/p&gt;

&lt;p&gt;Addressables exposes &lt;code&gt;WebRequestQueue.SetMaxConcurrentRequests(int)&lt;/code&gt;. Six is only a starting point; compare values such as four, six, and eight for your devices and bundle sizes.&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;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.AddressableAssets&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.Networking&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.ResourceManagement&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;AddressablesNetworkSettings&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="n"&gt;s_configured&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;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UnityWebRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s_existingOverride&lt;/span&gt;&lt;span class="p"&gt;;&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;BeforeSceneLoad&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&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;s_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="n"&gt;s_configured&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;WebRequestQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetMaxConcurrentRequests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;s_existingOverride&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Addressables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WebRequestOverride&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Addressables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WebRequestOverride&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ApplySettings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ApplySettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UnityWebRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;s_existingOverride&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&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="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Keep the default NotForced behavior and allow negotiation.&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 erase an override that adds signed URLs, authorization, certificates, or telemetry. Compose with it or centralize settings. The guard only covers one AppDomain; Domain Reload settings, multiple bootstraps, and tests still need one owner before the first Addressables operation.&lt;/p&gt;

&lt;p&gt;These APIs are from Addressables 2.9, listed as 2.9.1 in Unity 6.5.0f1. Other Unity 6.3–6.5 projects may differ; check &lt;code&gt;manifest.json&lt;/code&gt;, &lt;code&gt;packages-lock.json&lt;/code&gt;, and compile your shipped versions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.addressables%402.9/api/UnityEngine.ResourceManagement.WebRequestQueue.SetMaxConcurrentRequests.html" rel="noopener noreferrer"&gt;&lt;code&gt;WebRequestQueue.SetMaxConcurrentRequests&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.addressables%402.9/api/UnityEngine.AddressableAssets.Addressables.WebRequestOverride.html" rel="noopener noreferrer"&gt;&lt;code&gt;Addressables.WebRequestOverride&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When HTTP/3 is promising for assets
&lt;/h3&gt;

&lt;p&gt;Test HTTP/3 when iOS and Android dominate, users download over 4G/5G, dozens of files run together, packet loss stalls unrelated work, network transitions are common, the CDN supports UDP/443 and HTTP/3 advertisement, and p95/p99 completion time is a business problem.&lt;/p&gt;

&lt;p&gt;Tail latency, failure rate, and retry volume can matter more than the mean. An internal Wi-Fi-only application with few requests may never recover the plugin cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  One large file: prioritize resume and integrity
&lt;/h3&gt;

&lt;p&gt;A multi-gigabyte patch needs application-level resume:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;write to a temporary file;&lt;/li&gt;
&lt;li&gt;resume through &lt;code&gt;Accept-Ranges: bytes&lt;/code&gt; and a Range request;&lt;/li&gt;
&lt;li&gt;verify identity with an ETag or content hash;&lt;/li&gt;
&lt;li&gt;validate the final hash or signature;&lt;/li&gt;
&lt;li&gt;atomically replace the production file and retain the last known-good version on failure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;HTTP/3 does not manage resume position, corrupted partial data, or catalog consistency. Apple's &lt;a href="https://developer.apple.com/videos/play/wwdc2023/10006/" rel="noopener noreferrer"&gt;resumable file transfer session&lt;/a&gt; covers Range requests, ETags, background transfer, and URLSession resume behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you add native iOS or Android plugins?
&lt;/h2&gt;

&lt;p&gt;Ask whether measured gains justify another stack, not merely whether a plugin supports HTTP/3. Native integration adds C# bridging, cancellation, buffers, cookies, caches, certificates, build dependencies, and platform-specific debugging. Ordinary REST APIs should start with Unity's negotiated HTTP/2 path.&lt;/p&gt;

&lt;p&gt;A plugin is reasonable when tail latency or failures improve materially, background delivery is mandatory, or content delivery is central. The CDN must support UDP/443, TLS 1.3, ALPN &lt;code&gt;h3&lt;/code&gt;, &lt;code&gt;Alt-Svc&lt;/code&gt; or DNS HTTPS records, and HTTP/2 fallback.&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS: URLSession
&lt;/h3&gt;

&lt;p&gt;Apple's API provides negotiation, caching, Range requests, background transfer, and resume. For a known-capable service, &lt;code&gt;assumesHTTP3Capable&lt;/code&gt; can let URLSession attempt HTTP/3 before receiving an advertisement, but it does not guarantee &lt;code&gt;h3&lt;/code&gt; and still permits fallback. For large files, write to a native temporary file and report progress, path, and validation instead of copying a huge &lt;code&gt;byte[]&lt;/code&gt; through Unity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unity C#
  ├─ StartDownload(url, destination, requestId)
  ├─ Cancel(requestId)
  └─ OnProgress / OnComplete / OnError
             ↓
iOS plugin: URLSessionDownloadTask / background URLSession
             ↓
CDN: HTTP/3 → HTTP/2 → HTTP/1.1 fallback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Short-lived signed URLs can avoid duplicating token state. URLSession and &lt;code&gt;UnityWebRequest&lt;/code&gt; do not automatically share pools, cookies, cache, certificates, retries, or metrics. Native-enable a narrow path and assign ownership of authentication, cache deletion, and diagnostics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Android: HttpEngine or Cronet
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;th&gt;Protocols&lt;/th&gt;
&lt;th&gt;Main condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HttpEngine&lt;/td&gt;
&lt;td&gt;HTTP/1.1, HTTP/2, HTTP/3 over QUIC&lt;/td&gt;
&lt;td&gt;API 34 or S Extensions 7+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cronet via Google Play services&lt;/td&gt;
&lt;td&gt;HTTP/1.1, HTTP/2, HTTP/3 over QUIC&lt;/td&gt;
&lt;td&gt;Requires Play services; small size impact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedded Cronet&lt;/td&gt;
&lt;td&gt;HTTP/1.1, HTTP/2, HTTP/3 over QUIC&lt;/td&gt;
&lt;td&gt;Bundled implementation; documentation cites about 8 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OkHttp&lt;/td&gt;
&lt;td&gt;HTTP/1.1 and HTTP/2&lt;/td&gt;
&lt;td&gt;Not an HTTP/3 option&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use HttpEngine when the minimum platform permits it. For wider coverage, Play services Cronet plus fallback may work; Embedded Cronet is relevant where Play services are unavailable.&lt;/p&gt;

&lt;p&gt;Reuse engine instances. Running them beside &lt;code&gt;UnityWebRequest&lt;/code&gt; splits connection pools, DNS behavior, caches, cookies, certificates, and metrics. Migrate one high-value path first.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/develop/connectivity/cronet" rel="noopener noreferrer"&gt;Android Cronet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/media/media3/exoplayer/network-stacks" rel="noopener noreferrer"&gt;Android network stacks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Workload 3: real-time networking is a separate decision
&lt;/h2&gt;

&lt;p&gt;Faster REST is not 60 fps state synchronization. Real-time selection depends on reliability, stale-data handling, ordering, authority, prediction, rollback, tick rate, bandwidth, NAT traversal, reconnect, matchmaking, and platform restrictions.&lt;/p&gt;

&lt;p&gt;HTTP/3 uses QUIC, but an HTTP/3 REST endpoint is not automatically a game-state transport.&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;Candidate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Turn-based or asynchronous play&lt;/td&gt;
&lt;td&gt;HTTP API with normal negotiation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lobby, friends, and light notifications&lt;/td&gt;
&lt;td&gt;WebSocket or Photon Realtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chat&lt;/td&gt;
&lt;td&gt;WebSocket, Photon Chat, or gRPC streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Co-op or competitive action&lt;/td&gt;
&lt;td&gt;Photon Fusion, Unity Transport, or dedicated UDP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typed bidirectional streams or telemetry&lt;/td&gt;
&lt;td&gt;gRPC streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser low-latency communication&lt;/td&gt;
&lt;td&gt;WebSocket, WebRTC, and possibly WebTransport&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Photon does not replace asset HTTP. A common architecture uses HTTP APIs for account data, HTTP plus a CDN for bundles, and Photon for matchmaking and sessions. Web builds also have browser transport restrictions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://doc.photonengine.com/realtime/current/connection-and-authentication/tcp-and-udp-port-numbers" rel="noopener noreferrer"&gt;Photon protocol and ports&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;gRPC is typed RPC, not a universal action-game transport. HTTP/2 streaming fits internal APIs, telemetry, and typed streams, but not necessarily stale positions that should be dropped. Test channel reuse, stream limits, AOT, and generated code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://grpc.io/docs/guides/performance/" rel="noopener noreferrer"&gt;gRPC performance best practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Benchmark protocol choices correctly
&lt;/h2&gt;

&lt;p&gt;Do not decide from one Editor run on wired Ethernet. Test real iOS and Android devices under Wi-Fi, 4G/5G, 50–150 ms latency, 1–3% loss, network transitions, and cold versus reused connections. Record CDN &lt;code&gt;HIT&lt;/code&gt;/&lt;code&gt;MISS&lt;/code&gt;, local cache state, and first versus repeat download so cache effects are not mistaken for protocol gains.&lt;/p&gt;

&lt;p&gt;Compare TTFB, time to first playable, all-files completion, p50/p95/p99, failures, retries, retransferred bytes, CPU, peak memory, battery, and network-change recovery—not only the mean.&lt;/p&gt;

&lt;p&gt;“HTTP/3 enabled” at the CDN is not proof of use. Verify with CDN/origin logs, ALPN, iOS Instruments, the browser Protocol column, or Cronet's &lt;code&gt;getNegotiatedProtocol()&lt;/code&gt;. Do not assume ordinary &lt;code&gt;UnityWebRequest&lt;/code&gt; exposes &lt;code&gt;h2&lt;/code&gt; or &lt;code&gt;h3&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical selection flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Separate APIs, asset delivery, and real-time state.&lt;/li&gt;
&lt;li&gt;Establish HTTP/2 negotiation; fix dependencies, bundle layout, caching, Range support, and idempotency first.&lt;/li&gt;
&lt;li&gt;Measure latency, loss, transitions, tail latency, and failures on real devices.&lt;/li&gt;
&lt;li&gt;Prototype a native stack on one high-value path, and adopt it only when gains exceed maintenance and diagnostic cost.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep HTTP/2 fallback even after a successful HTTP/3 rollout.&lt;/p&gt;

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

&lt;p&gt;For most Unity projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use normal negotiation and HTTP/2 for ordinary APIs, with HTTP/1.1 as fallback;&lt;/li&gt;
&lt;li&gt;fix CDN caching, bundle layout, concurrency, Range requests, hashes, and resume logic before changing transports;&lt;/li&gt;
&lt;li&gt;use HTTP/3 only where mobile loss, parallel work, or network transitions show a measured benefit;&lt;/li&gt;
&lt;li&gt;add URLSession, HttpEngine, or Cronet only when native integration is justified;&lt;/li&gt;
&lt;li&gt;choose Photon, Unity Transport, WebSocket, or gRPC from real-time requirements, not the HTTP version number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is rarely a reason to move every request to HTTP/3 at once. &lt;strong&gt;Build a correct HTTP/2 path, measure real devices, and native-enable only the traffic where the data supports it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A follow-up can compare Photon, Unity Transport/Netcode, WebSocket, gRPC, WebRTC, WebTransport, and custom UDP by reliability, tick model, authority, browser support, cost, and operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Primary references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.3/Documentation/Manual/WhatsNewUnity63.html" rel="noopener noreferrer"&gt;Unity 6.3: What's new&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://unity.com/releases/editor/whats-new/6000.5.0f1" rel="noopener noreferrer"&gt;Unity 6.5 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Networking.UnityWebRequest-httpForcedVersion.html" rel="noopener noreferrer"&gt;&lt;code&gt;UnityWebRequest.httpForcedVersion&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9113.html" rel="noopener noreferrer"&gt;RFC 9113: HTTP/2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9114.html" rel="noopener noreferrer"&gt;RFC 9114: HTTP/3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>networking</category>
      <category>http</category>
    </item>
    <item>
      <title>Unity CLI + AI Agents: A Safe Observe-Act-Verify Workflow</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Tue, 01 Sep 2026 06:15:10 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/unity-cli-ai-agents-a-safe-observe-act-verify-workflow-5c9k</link>
      <guid>https://dev.to/gamedevtoollab/unity-cli-ai-agents-a-safe-observe-act-verify-workflow-5c9k</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Checked against the official documentation available on September 1, 2026. Unity CLI and &lt;code&gt;com.unity.pipeline&lt;/code&gt; are experimental, so the installed version's &lt;code&gt;unity --help&lt;/code&gt; is the authoritative reference.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why writing C# is not enough
&lt;/h2&gt;

&lt;p&gt;AI agents can edit Unity C#, but an edit is not a verified result. Unity may need to recompile and reimport assets; tests, Console errors, Scene state, Play Mode, or a built Player may also need inspection.&lt;/p&gt;

&lt;p&gt;A useful agent must close this loop without asking a human to copy every result from the Editor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observe -&amp;gt; Plan -&amp;gt; Act -&amp;gt; Wait -&amp;gt; Verify -&amp;gt; Recover
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unity's standalone CLI and &lt;code&gt;com.unity.pipeline&lt;/code&gt; provide an official execution layer for that loop. The focus here is a restricted workflow for inspection, mutation, and verification—not a complete command reference or vendor-specific CI file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the three command-line surfaces
&lt;/h2&gt;

&lt;p&gt;Unity developers often call three different systems “the CLI.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Traditional Editor arguments
&lt;/h3&gt;

&lt;p&gt;The Editor executable still accepts &lt;code&gt;-batchmode&lt;/code&gt;, &lt;code&gt;-projectPath&lt;/code&gt;, &lt;code&gt;-executeMethod&lt;/code&gt;, &lt;code&gt;-runTests&lt;/code&gt;, and related flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Unity.exe&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="nt"&gt;-quit&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;"C:\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;-executeMethod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BuildCommand.BuildWindows&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;"C:\Logs\unity-build.log"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This remains useful for headless jobs, but it launches an Editor process and locks the project; it does not control an already-open Editor.&lt;/p&gt;

&lt;h3&gt;
  
  
  The standalone Unity CLI
&lt;/h3&gt;

&lt;p&gt;The newer &lt;code&gt;unity&lt;/code&gt; executable manages Editors, modules, authentication, projects, builds, tests, and connected instances without opening the Hub UI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unity editors &lt;span class="nt"&gt;--installed&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; json
unity &lt;span class="nb"&gt;install &lt;/span&gt;6000.3.7f1 &lt;span class="nt"&gt;-m&lt;/span&gt; android
unity open ./MyGame
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It supports human, TSV, JSON, NDJSON, and GitHub-oriented output, with structured stdout, diagnostic stderr, and exit codes for automation.&lt;/p&gt;

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

&lt;p&gt;After adding &lt;code&gt;com.unity.pipeline&lt;/code&gt;, the CLI can discover commands registered by a running Editor or configured Development Player:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unity status &lt;span class="nt"&gt;--json&lt;/span&gt;
unity &lt;span class="nb"&gt;command
&lt;/span&gt;unity &lt;span class="nb"&gt;command &lt;/span&gt;get_console_logs &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Editor arguments&lt;/td&gt;
&lt;td&gt;Existing headless automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standalone CLI&lt;/td&gt;
&lt;td&gt;Editor/module management, projects, builds, tests, CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline&lt;/td&gt;
&lt;td&gt;Fast inspection and controlled operations in a running Editor or Player&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Design a small capability surface
&lt;/h2&gt;

&lt;p&gt;The agent reaches Unity through shell commands or the CLI's MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI agent -&amp;gt; unity command / unity mcp -&amp;gt; Unity CLI
         -&amp;gt; com.unity.pipeline -&amp;gt; Editor / Development Player
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not expose unlimited Unity API access as the default. Expose named commands with declared arguments and structured results. Your team can review that surface, restrict paths, require previews, and decide which operations need human approval.&lt;/p&gt;

&lt;p&gt;Start with these three commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unity &lt;span class="nt"&gt;--version&lt;/span&gt;
unity status &lt;span class="nt"&gt;--json&lt;/span&gt;
unity list &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They identify the CLI version, connected instance, and registered schemas. Next, add a read-only &lt;code&gt;project_health&lt;/code&gt; command. After each C# edit, require proof of final compilation, passing targeted tests, zero new Console errors, and no forbidden asset changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record versions and install deliberately
&lt;/h2&gt;

&lt;p&gt;As of September 1, 2026, the current documented releases are Unity CLI &lt;code&gt;1.0.0-beta.6&lt;/code&gt; and &lt;code&gt;com.unity.pipeline 0.5.0-exp.1&lt;/code&gt;. Store a compatibility record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unity Editor: 6000.x.yf1
Unity CLI: 1.0.0-beta.6
com.unity.pipeline: 0.5.0-exp.1
Verified: 2026-09-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;beta&lt;/code&gt; channel is not a version pin. Make CI fail when &lt;code&gt;unity --version&lt;/code&gt; differs, and avoid unconditional upgrades inside jobs.&lt;/p&gt;

&lt;p&gt;Install the CLI through an official route, then inspect it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS/Linux&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://public-cdn.cloud.unity3d.com/hub/prod/cli/install.sh &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nv"&gt;UNITY_CLI_CHANNEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;beta bash

&lt;span class="c"&gt;# macOS/Linux alternative&lt;/span&gt;
brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; unity-cli

unity &lt;span class="nt"&gt;--version&lt;/span&gt;
unity doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Windows&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Unity.CLI&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Installing modules with a new Editor and adding them later are separate operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unity &lt;span class="nb"&gt;install &lt;/span&gt;6000.3.7f1 &lt;span class="nt"&gt;-m&lt;/span&gt; android webgl
unity install-modules &lt;span class="nt"&gt;-e&lt;/span&gt; 6000.3.7f1 &lt;span class="nt"&gt;-m&lt;/span&gt; android webgl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the project, install Pipeline, wait for compilation, and inspect the connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/MyGame
unity auth login
unity pipeline list-versions
unity pipeline &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--package-version&lt;/span&gt; 0.5.0-exp.1
unity status &lt;span class="nt"&gt;--json&lt;/span&gt;
unity list &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm the exact Pipeline flags with local help. Pipeline-driven Editor control requires Unity 6.0 LTS or later. With several Editors open, run from the intended project or specify it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unity &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;--project-path&lt;/span&gt; /path/to/MyGame editor_status &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Old examples using &lt;code&gt;--instance &amp;lt;host:port&amp;gt;&lt;/code&gt; are stale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Direct commands or MCP?
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;unity command&lt;/code&gt; and &lt;code&gt;unity eval&lt;/code&gt; directly when the agent can compose shell commands reliably. Unity's current guidance notes that this path is faster and consumes fewer model tokens than MCP.&lt;/p&gt;

&lt;p&gt;Use the built-in MCP server when the client cannot run arbitrary commands or has a stronger tool-calling workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unity mcp configure &lt;span class="nt"&gt;--list&lt;/span&gt;
unity mcp configure &amp;lt;client&amp;gt;
unity skill &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;agent-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client configuration, approval UI, working directory, and inherited permissions vary. MCP is transport and discovery, not authorization; keep policy in the repository and CI.&lt;/p&gt;

&lt;p&gt;Unity has deprecated the MCP server bundled with the in-Editor AI assistant package in favor of &lt;code&gt;unity mcp&lt;/code&gt;; third-party MCP packages are a separate choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn every task into a closed loop
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Observe and Plan
&lt;/h3&gt;

&lt;p&gt;Capture the initial Editor state, open Scenes, dirty state, and existing errors. Define success mechanically: a named test passes, new-error count is zero, only approved files changed, and Scene or Prefab edits are forbidden unless requested.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unity status &lt;span class="nt"&gt;--json&lt;/span&gt;
unity &lt;span class="nb"&gt;command &lt;/span&gt;editor_status &lt;span class="nt"&gt;--json&lt;/span&gt;
unity &lt;span class="nb"&gt;command &lt;/span&gt;list_open_scenes &lt;span class="nt"&gt;--json&lt;/span&gt;
unity &lt;span class="nb"&gt;command &lt;/span&gt;get_console_logs &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Act and Wait
&lt;/h3&gt;

&lt;p&gt;Use normal file editing for persistent C#, typed &lt;code&gt;[CliCommand]&lt;/code&gt; methods for repeatable Unity operations, and &lt;code&gt;eval&lt;/code&gt; only for narrow investigation. Start and completion can be different states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;recompile -&amp;gt; recompile_status
run_tests -&amp;gt; test_status
build -&amp;gt; build_status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Domain reloads, imports, and target switches can briefly interrupt the connection. Retry only within a bounded policy and poll a final status rather than treating request acceptance as success.&lt;/p&gt;

&lt;p&gt;Pipeline &lt;code&gt;0.5.0-exp.1&lt;/code&gt; also supports detached jobs and live progress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unity &lt;span class="nb"&gt;command&lt;/span&gt; &amp;lt;long_command&amp;gt; &lt;span class="nt"&gt;--detach&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt;
unity job status &amp;lt;job-id&amp;gt; &lt;span class="nt"&gt;--json&lt;/span&gt;
unity job &lt;span class="nb"&gt;wait&lt;/span&gt; &amp;lt;job-id&amp;gt; &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detached jobs survive a client timeout, but remain serialized and do not survive domain reload.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify and Recover
&lt;/h3&gt;

&lt;p&gt;Match evidence to the artifact:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Required evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;C#&lt;/td&gt;
&lt;td&gt;final compile state, targeted tests, new Console errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scene/Prefab&lt;/td&gt;
&lt;td&gt;serialized values, dirty/save state, Git diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;Play Mode state, Game View capture, logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Player state, Player log, exceptions, capture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build&lt;/td&gt;
&lt;td&gt;final result, output path, artifact checks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Report commands, exit codes, test counts, new errors, changed paths, and outputs. “It looks fixed” is not evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a typed read-only command
&lt;/h2&gt;

&lt;p&gt;A custom command is a static method marked with &lt;code&gt;[CliCommand]&lt;/code&gt;; arguments use &lt;code&gt;[CliArg]&lt;/code&gt;. The example below depends on &lt;code&gt;UnityEditor&lt;/code&gt;, so place it in an &lt;code&gt;Editor&lt;/code&gt; folder or Editor-only Assembly Definition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#if UNITY_EDITOR
&lt;/span&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.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;Unity.Pipeline.Commands&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;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine.SceneManagement&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;ProjectHealthCommands&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CliCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"project_health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Return editor and scene state."&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;object&lt;/span&gt; &lt;span class="nf"&gt;GetProjectHealth&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;scenes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Enumerable&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;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SceneManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sceneCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SceneManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetSceneAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&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="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;s&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isLoaded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isDirty&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;unityVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unityVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;isPlaying&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EditorApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isPlaying&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;isCompiling&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EditorApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isCompiling&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;buildTarget&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EditorUserBuildSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activeBuildTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;scenes&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unity &lt;span class="nb"&gt;command &lt;/span&gt;project_health &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MainThreadRequired&lt;/code&gt; defaults to &lt;code&gt;true&lt;/code&gt;; keep it unless the implementation is independent of main-thread-only Unity APIs. Return fields, not prose, so an agent can assert them. At the Pipeline layer, a response conceptually resembles:&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;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"project_health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&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;"isCompiling"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scenes"&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;"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;"Main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"isDirty"&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;span class="nl"&gt;"executionTimeMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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;For failures, add stable codes such as &lt;code&gt;INVALID_ARGUMENT&lt;/code&gt;, &lt;code&gt;NOT_FOUND&lt;/code&gt;, or &lt;code&gt;PRECONDITION_FAILED&lt;/code&gt; to your own result contract. Runtime commands belong in a Player-included assembly with no &lt;code&gt;UnityEditor&lt;/code&gt; dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep &lt;code&gt;eval&lt;/code&gt; for investigation
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;eval&lt;/code&gt; is useful for a one-off read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unity &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"return UnityEngine.Object.FindObjectsByType&amp;lt;UnityEngine.Light&amp;gt;(UnityEngine.FindObjectsSortMode.None).Length;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is close to arbitrary C# execution. Never send model-generated or prompt-derived code to it without review. Limit it to short, human-inspected, read-only expressions and do not concatenate untrusted external strings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;One-off inspection          -&amp;gt; eval
Repeated read               -&amp;gt; read-only CliCommand
Scene/Prefab authoring       -&amp;gt; typed CliCommand + Undo
Destructive asset operation -&amp;gt; dry_run + confirm + Git/backup
Stable CI task              -&amp;gt; unity run --command / unity test / unity build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Preview mutations and separate approval
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dry_run&lt;/code&gt; and &lt;code&gt;confirm&lt;/code&gt; are per-command conventions; Pipeline does not provide a central approval service.&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;Unity.Pipeline.Commands&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CliCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"normalize_enemy_layers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Preview before applying."&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;object&lt;/span&gt; &lt;span class="nf"&gt;NormalizeEnemyLayers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CliArg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"confirm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Apply changes."&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;confirm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CliArg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dry_run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Preview only."&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;dryRun&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;FindTargets&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;dryRun&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="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"dry_run"&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;targets&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;samplePaths&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="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AssetPath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;warnings&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;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;50&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;"Target count exceeds 50."&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="n"&gt;Empty&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;requiresApproval&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;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;20&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="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSceneOrPrefab&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;confirm&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="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&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;"Pass confirm=true or dry_run=true."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;ApplyWithUndo&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"applied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;changed&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;Length&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;FindTargets&lt;/code&gt; and &lt;code&gt;ApplyWithUndo&lt;/code&gt; are project-specific placeholders.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unity &lt;span class="nb"&gt;command &lt;/span&gt;normalize_enemy_layers &lt;span class="nt"&gt;--dry_run&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt;
unity &lt;span class="nb"&gt;command &lt;/span&gt;normalize_enemy_layers &lt;span class="nt"&gt;--confirm&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Return a count, representative paths, warnings, and approval requirement instead of every target. This limits token usage and accidental disclosure of unpublished asset paths.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;confirm=true&lt;/code&gt; is only an intent flag. It does not record who approved what, so it cannot replace a human approval log or pull-request diff. Automatic application should be limited to approved generated paths, no deletion, strict count limits, a clean dedicated worktree, and deterministic verification.&lt;/p&gt;

&lt;p&gt;Require human approval for Scenes, Prefabs, &lt;code&gt;ProjectSettings&lt;/code&gt;, &lt;code&gt;Packages&lt;/code&gt;, deletion, bulk overwrites, external I/O, signing, or publishing. &lt;code&gt;AuthoringUndoScope&lt;/code&gt; can group registered Scene/Object Undo operations, but AssetDatabase, Package Manager, and some settings changes need Git or backup recovery. Restrict file I/O to approved roots and reject absolute paths and &lt;code&gt;..&lt;/code&gt; traversal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parse JSON and exit codes at every layer
&lt;/h2&gt;

&lt;p&gt;Use JSON for a buffered result and NDJSON for machine-readable progress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unity editors &lt;span class="nt"&gt;--installed&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; json
unity &lt;span class="nb"&gt;install &lt;/span&gt;6000.3.7f1 &lt;span class="nt"&gt;--format&lt;/span&gt; ndjson
unity &lt;span class="nb"&gt;command &lt;/span&gt;project_health &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two result layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Typical fields&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standalone CLI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;success&lt;/code&gt;, &lt;code&gt;command&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;errors&lt;/code&gt;, &lt;code&gt;warnings&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline response&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;success&lt;/code&gt;, &lt;code&gt;command&lt;/code&gt;, &lt;code&gt;result&lt;/code&gt;, &lt;code&gt;executionTimeMs&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Conceptually, the Pipeline response can be nested under CLI &lt;code&gt;data&lt;/code&gt;. That is a parsing model, not a promise that every experimental build has an identical shape. Capture your installed version's actual &lt;code&gt;unity command ... --json&lt;/code&gt; output before freezing a parser.&lt;/p&gt;

&lt;p&gt;Unity CLI &lt;code&gt;1.0.0-beta.6&lt;/code&gt; documents these process exit codes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;General error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Invalid usage or arguments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Authentication or authorization failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Required configuration or context missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Primary operation failed; &lt;code&gt;unity test&lt;/code&gt; produced no verdict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Unity service unreachable after retries; safe to retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;unity test&lt;/code&gt; ran and one or more tests failed; do not retry as infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;130&lt;/td&gt;
&lt;td&gt;Ctrl+C / SIGINT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;143&lt;/td&gt;
&lt;td&gt;SIGTERM or runner timeout&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 shell"&gt;&lt;code&gt;unity &lt;span class="nb"&gt;command &lt;/span&gt;project_health &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  1&amp;gt; result.json &lt;span class="se"&gt;\&lt;/span&gt;
  2&amp;gt; error.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Evaluate success in order: process exit code, outer CLI &lt;code&gt;success&lt;/code&gt;, inner Pipeline &lt;code&gt;success&lt;/code&gt;, then domain results such as failed tests or build output. Exit code &lt;code&gt;0&lt;/code&gt; only proves success at the process layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the running Editor from clean CI
&lt;/h2&gt;

&lt;p&gt;A running Editor is ideal for current Scene state, Console inspection, targeted tests, small authoring operations, Play Mode, and exploratory reads. Use a fresh process and workspace for pull requests, full suites, deterministic generation, and release builds.&lt;/p&gt;

&lt;p&gt;The same registered command can run headlessly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unity run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project-path&lt;/span&gt; ./MyGame &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--command&lt;/span&gt; validate_release_assets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Connect to a Development Player
&lt;/h2&gt;

&lt;p&gt;Runtime connectivity targets Windows, macOS, or Linux standalone Development Builds, not release builds. Mobile, console, and WebGL are outside this article's scope.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add &lt;code&gt;Pipeline &amp;gt; Runtime Pipeline Manager&lt;/code&gt; to a startup Scene GameObject.&lt;/li&gt;
&lt;li&gt;Enable &lt;code&gt;enableInBuilds&lt;/code&gt;; normally keep &lt;code&gt;autoStart&lt;/code&gt; enabled and port &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Make and launch a standalone Development Build.&lt;/li&gt;
&lt;li&gt;Wait for its runtime descriptor.&lt;/li&gt;
&lt;li&gt;Put &lt;code&gt;--runtime&lt;/code&gt; or &lt;code&gt;--runtime-path&lt;/code&gt; immediately after &lt;code&gt;command&lt;/code&gt;.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unity &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;--runtime&lt;/span&gt; MyGame.exe runtime_status &lt;span class="nt"&gt;--json&lt;/span&gt;
unity &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;--runtime-path&lt;/span&gt; &lt;span class="s2"&gt;"C:&lt;/span&gt;&lt;span class="se"&gt;\B&lt;/span&gt;&lt;span class="s2"&gt;uilds&lt;/span&gt;&lt;span class="se"&gt;\M&lt;/span&gt;&lt;span class="s2"&gt;yGame"&lt;/span&gt; runtime_status &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runtime server code is compiled only into Development Players. &lt;code&gt;enableInBuilds=true&lt;/code&gt; does not create a release-build backdoor. Keep custom runtime commands in a Player assembly and return structured state—Scene, position, gameplay state, exception count—alongside logs or captures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local-only still requires security controls
&lt;/h2&gt;

&lt;p&gt;Pipeline binds to local loopback and authenticates with a bearer token from a descriptor file. Let the CLI handle discovery. A custom HTTP client should use &lt;code&gt;127.0.0.1&lt;/code&gt; explicitly because &lt;code&gt;localhost&lt;/code&gt; can resolve to IPv6 &lt;code&gt;::1&lt;/code&gt;, which is unreliable on the relevant Mono &lt;code&gt;HttpListener&lt;/code&gt; path.&lt;/p&gt;

&lt;p&gt;The agent still has repository and shell access. Use a dedicated branch or worktree, inspect &lt;code&gt;git status&lt;/code&gt;, and expand permissions from read-only inspection to narrowly scoped mutations.&lt;/p&gt;

&lt;p&gt;Treat logs, captures, descriptor files, credentials, signing material, IDs, and unreleased art as sensitive; define what may reach a cloud model.&lt;/p&gt;

&lt;p&gt;Put the operational rules in &lt;code&gt;AGENTS.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Unity workflow rules&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Use ProjectSettings/ProjectVersion.txt as the Editor version source.
&lt;span class="p"&gt;-&lt;/span&gt; Run unity status and project_health before changes.
&lt;span class="p"&gt;-&lt;/span&gt; Check git status before touching Scenes, Prefabs, or settings.
&lt;span class="p"&gt;-&lt;/span&gt; Run dry_run first; dry_run is not approval.
&lt;span class="p"&gt;-&lt;/span&gt; Require human approval or PR review for Scenes, Prefabs,
  ProjectSettings, Packages, deletion, and bulk updates.
&lt;span class="p"&gt;-&lt;/span&gt; Use eval only for reviewed, read-only investigation.
&lt;span class="p"&gt;-&lt;/span&gt; After C# edits, wait for compilation, run targeted tests,
  and prove that new Console error count is zero.
&lt;span class="p"&gt;-&lt;/span&gt; Never sign or upload a release.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Editor found:&lt;/strong&gt; check &lt;code&gt;unity status --json&lt;/code&gt;, &lt;code&gt;unity editors running --json&lt;/code&gt;, &lt;code&gt;unity pipeline list --json&lt;/code&gt;, compilation state, and working directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Command missing:&lt;/strong&gt; the method must be static, compile successfully, and live in the correct Editor or Player assembly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale flags:&lt;/strong&gt; read &lt;code&gt;unity --help&lt;/code&gt;, command-specific help, and &lt;code&gt;unity list --json&lt;/code&gt; after upgrades.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporary disconnect:&lt;/strong&gt; bound retries and poll final status after domain reload, import, target switch, or the Pipeline &lt;code&gt;0.5&lt;/code&gt; startup settling state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False test success:&lt;/strong&gt; request acceptance is not a passing suite; poll &lt;code&gt;test_status&lt;/code&gt; or inspect the NUnit report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsaved Scene:&lt;/strong&gt; report dirty state and make saving a separate explicit operation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undo gap:&lt;/strong&gt; use previews, Git, and backups for AssetDatabase, packages, and settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A staged adoption plan
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read:&lt;/strong&gt; status, hierarchy, Console, test list, and &lt;code&gt;project_health&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify:&lt;/strong&gt; bounded compile waits, targeted tests, Play Mode, and captures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Author narrowly:&lt;/strong&gt; typed commands with preview, limits, approval, Undo, and Git diffs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect runtime:&lt;/strong&gt; combine Development Player state, logs, exceptions, and captures.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal is a small command set with explicit preconditions, side effects, outputs, and recovery paths.&lt;/p&gt;

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

&lt;p&gt;Unity CLI integration matters when it turns an AI edit into a verified engineering operation. Separate the standalone CLI, traditional Editor arguments, and Pipeline. Use &lt;code&gt;eval&lt;/code&gt; only for narrow investigation, promote repeated work into typed commands, and require every task to observe, act, wait, and verify.&lt;/p&gt;

&lt;p&gt;Combine &lt;code&gt;dry_run&lt;/code&gt;, &lt;code&gt;confirm&lt;/code&gt;, limits, Undo, Git, and human approval instead of trusting one safety mechanism. Keep the fast running-Editor loop separate from clean CI, and treat local help plus captured JSON as the source of truth for the experimental versions you run.&lt;/p&gt;

&lt;p&gt;Begin with read-only &lt;code&gt;project_health&lt;/code&gt;; expand only after the agent can prove the before-and-after state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Official references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://unity.com/blog/meet-the-unity-cli" rel="noopener noreferrer"&gt;Meet the Unity CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity.com/en-us/unity-cli/unity-cli-reference" rel="noopener noreferrer"&gt;Unity CLI reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity.com/en-us/unity-cli/use-unity-cli" rel="noopener noreferrer"&gt;Use the Unity CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity.com/en-us/unity-cli/release-notes" rel="noopener noreferrer"&gt;Unity CLI release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity.com/en-us/unity-cli/replace-mcp-server-unity-cli" rel="noopener noreferrer"&gt;Replace the in-Editor MCP server with Unity CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.pipeline@0.5/manual/creating-commands.html" rel="noopener noreferrer"&gt;Creating commands: Pipeline 0.5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.pipeline@0.5/manual/commands/build-and-compilation.html" rel="noopener noreferrer"&gt;Build, compilation, and tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.pipeline@0.5/manual/connectivity.html" rel="noopener noreferrer"&gt;Connectivity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.pipeline@0.5/manual/safety-and-mutations.html" rel="noopener noreferrer"&gt;Safety and mutations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.pipeline@0.5/manual/runtime-setup.html" rel="noopener noreferrer"&gt;Runtime setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.pipeline@0.5/changelog/CHANGELOG.html" rel="noopener noreferrer"&gt;Pipeline changelog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>ai</category>
      <category>gamedev</category>
      <category>cli</category>
    </item>
  </channel>
</rss>
