<?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>R3 Operation Rules for Unity 6: SubscribeAwait, EveryUpdate, and Patterns to Avoid</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Wed, 08 Jul 2026 06:52:48 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/r3-operation-rules-for-unity-6-subscribeawait-everyupdate-and-patterns-to-avoid-1l63</link>
      <guid>https://dev.to/gamedevtoollab/r3-operation-rules-for-unity-6-subscribeawait-everyupdate-and-patterns-to-avoid-1l63</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the previous article, I covered the basic design rules for using R3 in Unity 6 projects: how to expose &lt;code&gt;ReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt; and &lt;code&gt;Subject&amp;lt;T&amp;gt;&lt;/code&gt;, how to manage subscription lifetimes, and how to separate R3 from &lt;code&gt;async/await&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This article continues from there and focuses on &lt;strong&gt;operation rules after introducing R3&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The target reader is a Unity team with around 10 programmers, where multiple people touch UI, networking, loading, presentation, and game flow. In solo development or short-lived prototypes, you may not need to be this strict. This article intentionally takes a conservative approach that prioritizes maintainability in long-running projects.&lt;/p&gt;

&lt;p&gt;This is not an article about Unity 6-specific APIs. It is about how to operate R3 safely in Unity 6-era projects. Most of the ideas also apply to other Unity versions supported by R3.&lt;/p&gt;

&lt;p&gt;The code samples omit common &lt;code&gt;using&lt;/code&gt; declarations such as &lt;code&gt;using R3;&lt;/code&gt;, &lt;code&gt;using UnityEngine;&lt;/code&gt;, TextMeshPro, R3.Unity uGUI extensions, and &lt;code&gt;CancellationToken&lt;/code&gt;. Methods such as &lt;code&gt;OnClickAsObservable()&lt;/code&gt; and &lt;code&gt;OnValueChangedAsObservable()&lt;/code&gt; assume R3.Unity uGUI extensions. In Unity projects, you need both the R3 core package and R3.Unity, and their versions should be aligned.&lt;/p&gt;

&lt;p&gt;This article assumes R3 1.2.0 or later. In R3 1.2.0, the default value of &lt;code&gt;cancelOnCompleted&lt;/code&gt; for &lt;code&gt;SubscribeAwait&lt;/code&gt;, &lt;code&gt;SelectAwait&lt;/code&gt;, &lt;code&gt;WhereAwait&lt;/code&gt;, and &lt;code&gt;ReactiveCommand&lt;/code&gt; was changed to &lt;code&gt;false&lt;/code&gt;. When combining &lt;code&gt;Take(1)&lt;/code&gt;, disposal, completion, and async work, distinguish between stopping further input and canceling the currently running operation.&lt;/p&gt;

&lt;p&gt;The three rules to take away are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SubscribeAwait(Drop)&lt;/code&gt; prevents duplicate execution while an operation is running; it is not the same as simultaneous-input protection or one-shot control.&lt;/li&gt;
&lt;li&gt;PlayerLoop/time-based streams such as &lt;code&gt;EveryUpdate&lt;/code&gt; and &lt;code&gt;Timer&lt;/code&gt; should make purpose, lifetime, and provider explicit.&lt;/li&gt;
&lt;li&gt;Use R3 for notification and wiring; keep domain logic and important mutual exclusion readable as ordinary C#.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Use SubscribeAwait for repeated-click protection
&lt;/h2&gt;

&lt;p&gt;A common UI problem is repeated clicks during a network request or loading operation.&lt;/p&gt;

&lt;p&gt;R3 provides &lt;code&gt;SubscribeAwait&lt;/code&gt; and &lt;code&gt;SelectAwait&lt;/code&gt;. They let you specify how to handle the next event while an async operation is still running by using &lt;code&gt;AwaitOperation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want to ignore clicks while a request is running, use &lt;code&gt;Drop&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_loginButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoginAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Drop&lt;/code&gt; discards &lt;strong&gt;subsequent events that arrive while the async operation is running&lt;/strong&gt;. It is useful for login buttons, purchase buttons, gacha buttons, and confirm buttons where duplicate execution during processing must be avoided.&lt;/p&gt;

&lt;p&gt;However, &lt;code&gt;Drop&lt;/code&gt; does not mean "execute only once forever." If the operation finishes quickly, the next click will be processed normally. If the dialog stays open after the operation, input can still be accepted again. If you want to accept only the first input for the whole screen or dialog session, use a separate mechanism such as &lt;code&gt;Take(1)&lt;/code&gt;, a confirmed flag, explicit disposal, or closing the screen.&lt;/p&gt;

&lt;p&gt;For a search box where you want to cancel the previous request and process only the latest text, &lt;code&gt;Switch&lt;/code&gt; is usually a better fit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_searchInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnValueChangedAsObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Use this when initial-value search is not needed.&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMilliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;UnityTimeProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;SearchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Switch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In R3.Unity, &lt;code&gt;OnValueChangedAsObservable()&lt;/code&gt; emits the latest value at the time of subscription. If initial-value search is part of the specification, that is fine. If you do not want to call an API with an empty string or initial value during screen initialization, add &lt;code&gt;Skip(1)&lt;/code&gt; or an explicit filter. The &lt;code&gt;Debounce&lt;/code&gt; provider also matters: should it run while the game is paused, or should it follow &lt;code&gt;Time.timeScale&lt;/code&gt;? In this example, &lt;code&gt;UnityTimeProvider.Update&lt;/code&gt; is specified explicitly.&lt;/p&gt;

&lt;p&gt;A simple team rule can start like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Recommended operation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Login, purchase, confirm, gacha&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Drop&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search, preview update&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Switch&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Must process in order&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Sequential&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safe to run in parallel&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Parallel&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For ordinary game UI, avoid using &lt;code&gt;Parallel&lt;/code&gt; casually. It is not common for multiple purchases, transitions, or network operations to be safe when executed at the same time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repeated-click protection and simultaneous-input protection are different
&lt;/h2&gt;

&lt;p&gt;Repeated-click protection and simultaneous-input protection are not the same thing.&lt;/p&gt;

&lt;p&gt;Repeated-click protection handles pressing the same button many times in a short period. Simultaneous-input protection handles multiple buttons, back keys, shortcuts, gamepad inputs, or other input paths firing at nearly the same time.&lt;/p&gt;

&lt;p&gt;If each button has its own &lt;code&gt;SubscribeAwait(Drop)&lt;/code&gt;, different buttons are not mutually exclusive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_okButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;DecideAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;_cancelButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;CancelAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this form, pressing &lt;code&gt;OK&lt;/code&gt; and &lt;code&gt;Cancel&lt;/code&gt; almost simultaneously can still run both operations. &lt;code&gt;Drop&lt;/code&gt; is applied per Observable stream.&lt;/p&gt;

&lt;p&gt;If operations should be mutually exclusive, first merge them into one event stream.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;DialogAction&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Cancel&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;_okButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DialogAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;_cancelButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DialogAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ExecuteDialogActionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;OK&lt;/code&gt; and &lt;code&gt;Cancel&lt;/code&gt; belong to the same mutual-exclusion group. If another event arrives while the operation is running, it will be dropped.&lt;/p&gt;

&lt;p&gt;Still, this is only duplicate-execution prevention while the operation is running. If a confirmation dialog must accept exactly one action and never accept another one afterward, use session-level one-shot control.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;_okButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DialogAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;_cancelButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DialogAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ExecuteDialogActionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cancelOnCompleted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Take(1)&lt;/code&gt; stops input after the first event. That is separate from whether the async operation already in progress should be canceled or allowed to finish. For purchases, payments, server state changes, and gacha confirmation, you must also design cancellation, partial success, retry behavior, and idempotency.&lt;/p&gt;

&lt;p&gt;Another common pattern is to keep an explicit confirmed/closed flag.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="nf"&gt;ExecuteDialogActionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DialogAction&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_closed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;_closed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;SetButtonsInteractable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is also safer to put a shared gate on the processing side, not only in the UI stream. Input paths tend to grow over time: shortcuts, gamepads, keyboard back, test hooks, or direct method calls.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="nf"&gt;ExecuteDialogActionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DialogAction&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_isProcessing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;_isProcessing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Set this at the very beginning of the entry point.&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;SetButtonsInteractable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;DialogAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;DecideAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;DialogAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;CancelAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_isProcessing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activeInHierarchy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;SetButtonsInteractable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;bool&lt;/code&gt; gate is a simple example for UI / Presenter code running on the Unity main thread. If the operation can be called from a service layer, background thread, SDK callback, or multiple threads, use an appropriate synchronization method such as &lt;code&gt;SemaphoreSlim&lt;/code&gt;, &lt;code&gt;Interlocked&lt;/code&gt;, &lt;code&gt;lock&lt;/code&gt;, or a dedicated job queue.&lt;/p&gt;

&lt;p&gt;Also be careful with &lt;code&gt;finally&lt;/code&gt;. Re-enabling buttons unconditionally can be wrong if the view was destroyed, the screen was closed, or the button was disabled for another reason. In production code, choose a policy such as restoring the previous interactable state, binding a processing state from the ViewModel, or not re-enabling UI when the screen closes.&lt;/p&gt;

&lt;p&gt;A useful way to separate responsibilities is this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;View&lt;/td&gt;
&lt;td&gt;Merge multiple input paths into one entry point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observable stream&lt;/td&gt;
&lt;td&gt;Drop later events while processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Presenter / ViewModel&lt;/td&gt;
&lt;td&gt;Hold shared gates and confirmed states&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service / Server&lt;/td&gt;
&lt;td&gt;Provide idempotency and duplicate-execution protection for important operations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A good team rule is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;UI operations that must not run together should not be handled by independent per-button subscriptions. Merge them as one mutual-exclusion group and pass them through one &lt;code&gt;SubscribeAwait(Drop)&lt;/code&gt; or one shared gate.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Do not overuse EveryUpdate
&lt;/h2&gt;

&lt;p&gt;R3 provides &lt;code&gt;Observable.EveryUpdate()&lt;/code&gt;. It is convenient because it creates a stream that emits every frame. But if everything becomes &lt;code&gt;EveryUpdate&lt;/code&gt;, the code can become harder to follow than a normal &lt;code&gt;Update&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;A bad example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EveryUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;Rotate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;CheckGround&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;UpdateAnimation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is little benefit in writing this with R3. A normal &lt;code&gt;Update&lt;/code&gt; is clearer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;Rotate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;CheckGround&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;UpdateAnimation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;EveryUpdate&lt;/code&gt; is useful when you want to treat frame updates as an event stream and compose it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EveryUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsGrounded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;OnLanded&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or when you want to observe something for a fixed number of frames:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EveryUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;UpdatePreview&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, if &lt;code&gt;UpdatePreview()&lt;/code&gt; performs heavy work such as rebuilding UI or generating meshes, running it for 60 consecutive frames may still be too expensive. This is not an R3-specific issue; be careful with the actual work performed every frame.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Use normal &lt;code&gt;Update&lt;/code&gt; for ordinary game-loop logic.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;EveryUpdate&lt;/code&gt; only when frame events need stream composition.&lt;/li&gt;
&lt;li&gt;Long-lived &lt;code&gt;EveryUpdate&lt;/code&gt; subscriptions should have a clear reason.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EveryUpdate().Subscribe(...)&lt;/code&gt; with no meaningful composition should be questioned in review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need FixedUpdate-like timing, R3 can use &lt;code&gt;UnityFrameProvider.FixedUpdate&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EveryUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UnityFrameProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FixedUpdate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CheckFixedFrameEvent&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even so, physics logic itself is often clearer in a normal &lt;code&gt;FixedUpdate()&lt;/code&gt;. Use R3 here only when you need to compose events that happen on the FixedUpdate timing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Be explicit about TimeProvider and FrameProvider
&lt;/h2&gt;

&lt;p&gt;R3 can specify providers for time-based and frame-based operations. In Unity, the defaults are the Update-based providers: &lt;code&gt;UnityTimeProvider.Update&lt;/code&gt; and &lt;code&gt;UnityFrameProvider.Update&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;Timer&lt;/code&gt; or &lt;code&gt;Interval&lt;/code&gt; when waiting by time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;UnityTimeProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ShowMessage&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the operation should ignore &lt;code&gt;Time.timeScale&lt;/code&gt;, use &lt;code&gt;UpdateIgnoreTimeScale&lt;/code&gt; explicitly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;UnityTimeProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdateIgnoreTimeScale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ShowMessage&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to wait for a number of frames, use frame-based APIs instead of seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TimerFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UnityFrameProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ShowAfter60Frames&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Omitting the provider is not always wrong. But in games, whether a timer should stop during pause, ignore &lt;code&gt;timeScale&lt;/code&gt;, or be frame-based is part of the specification. For important code, specifying the provider makes review easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Use Observable Tracker for review and investigation
&lt;/h2&gt;

&lt;p&gt;R3 has Observable Tracker, a debugging aid for inspecting Observable and subscription state.&lt;/p&gt;

&lt;p&gt;When a team first introduces R3, common problems include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Subscriptions remain after closing a screen.&lt;/li&gt;
&lt;li&gt;Opening the same view increases the subscription count.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EveryUpdate&lt;/code&gt; keeps running longer than intended.&lt;/li&gt;
&lt;li&gt;Nobody knows which Observable is still alive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Observable Tracker is useful for investigating these problems.&lt;/p&gt;

&lt;p&gt;That said, it should not replace code review. The basic rule is to decide lifetimes in code first, then use Observable Tracker for suspicious areas or during the initial adoption phase.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do not let R3 leak too deeply into domain logic
&lt;/h2&gt;

&lt;p&gt;Once R3 is introduced, it can be tempting to turn everything into Observable streams. But if the core domain logic also becomes reactive, people who are not comfortable with R3 may struggle to read or modify it.&lt;/p&gt;

&lt;p&gt;For example, damage calculation is often just ordinary C#.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no need to express this calculation itself as an Observable chain.&lt;/p&gt;

&lt;p&gt;Use R3 to notify the result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_dead&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dead&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_dead&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_dead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In real code, the class that owns the &lt;code&gt;Subject&lt;/code&gt; should also dispose it.&lt;/p&gt;

&lt;p&gt;The basic policy is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep calculations, decisions, and state transitions as ordinary C#.&lt;/li&gt;
&lt;li&gt;Use R3 for state-change notifications, UI updates, and event wiring.&lt;/li&gt;
&lt;li&gt;Do not bury important specifications deep inside Observable chains.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters a lot in team development. If only the R3 expert can safely change the code, maintainability has already gone down.&lt;/p&gt;




&lt;h2&gt;
  
  
  Split complex chains
&lt;/h2&gt;

&lt;p&gt;Short R3 chains can be readable. Long ones become hard to review quickly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsVisible&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SortOrder&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;UpdateList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is still understandable, but real projects often add more conditions.&lt;/p&gt;

&lt;p&gt;When a chain grows, split it into named intermediate Observables or ordinary methods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;visibleItems&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FilterVisibleItems&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sortedItems&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;visibleItems&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SortItems&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;sortedItems&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateList&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;FilterVisibleItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsVisible&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SortItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SortOrder&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Named methods are easier to review than business rules hidden inside a long chain.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;ToReadOnlyReactiveProperty()&lt;/code&gt; when the result should be stored as state. Remember that the generated &lt;code&gt;ReadOnlyReactiveProperty&lt;/code&gt; also owns upstream subscriptions, so it needs a lifetime too.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do not overuse ToObservable
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ToObservable()&lt;/code&gt; is useful, but not everything should become an Observable.&lt;/p&gt;

&lt;p&gt;If you only need to calculate a total, LINQ is enough.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no need to write this as an Observable chain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;R3 is strong when values flow over time. If you are just aggregating an in-memory collection right now, LINQ or a normal &lt;code&gt;foreach&lt;/code&gt; is usually clearer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do not rely on AddTo(this) for every lifetime
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AddTo(this)&lt;/code&gt; is the basic way to tie a subscription to the lifetime of a &lt;code&gt;MonoBehaviour&lt;/code&gt; or &lt;code&gt;GameObject&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateHp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;AddTo(this)&lt;/code&gt; usually means lifetime until Destroy. It does not automatically mean disabled, hidden, tab-switched, popup-closed, or re-bound.&lt;/p&gt;

&lt;p&gt;If a tab creates subscriptions every time it is selected, use a separate &lt;code&gt;DisposableBag&lt;/code&gt; for that tab binding.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;BindTab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TabModel&lt;/span&gt; &lt;span class="n"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_tabDisposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_tabDisposables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateItems&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;_tabDisposables&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnDestroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_tabDisposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a disposed &lt;code&gt;DisposableBag&lt;/code&gt; receives a new disposable, the new one is disposed immediately. Reset it with &lt;code&gt;= default&lt;/code&gt; before reusing the field.&lt;/p&gt;

&lt;p&gt;The same applies to Views whose &lt;code&gt;Initialize()&lt;/code&gt; method may be called multiple times.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_bindDisposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_bindDisposables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateHp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;_bindDisposables&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a prefab is reused, dependency injection runs again, or a tab is rebound, &lt;code&gt;AddTo(this)&lt;/code&gt; alone can leave the previous subscription alive. Distinguish between GameObject lifetime, display lifetime, and binding lifetime.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to think about OnErrorResume
&lt;/h2&gt;

&lt;p&gt;R3 does not use the traditional Rx model where &lt;code&gt;OnError&lt;/code&gt; stops the subscription. Instead, errors are handled through &lt;code&gt;OnErrorResume&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For UI events, this is often convenient: a failed click should not necessarily kill the button subscription forever. But it also does not mean exceptions can be swallowed silently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_loginButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoginAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;OperationCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activeInHierarchy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;_dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Login failed."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second argument is R3's &lt;code&gt;OnErrorResume&lt;/code&gt; handler, not the old Rx &lt;code&gt;OnError&lt;/code&gt;. The subscription does not stop.&lt;/p&gt;

&lt;p&gt;In production code, decide these rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send logs to &lt;code&gt;Debug.LogException&lt;/code&gt; or the project's standard logger.&lt;/li&gt;
&lt;li&gt;Separate user-facing failure messages from developer logs.&lt;/li&gt;
&lt;li&gt;Decide whether &lt;code&gt;OperationCanceledException&lt;/code&gt; should be shown to users.&lt;/li&gt;
&lt;li&gt;Do not touch a destroyed View after screen transition.&lt;/li&gt;
&lt;li&gt;For important operations, consider representing success/failure as a return value instead of relying only on Observable error handling.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Patterns to avoid
&lt;/h2&gt;

&lt;p&gt;These are the patterns I would explicitly ban in team development.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern to avoid&lt;/th&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Alternative&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;public ReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anyone can write to it&lt;/td&gt;
&lt;td&gt;Keep it private and expose &lt;code&gt;ReadOnlyReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;public Subject&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anyone can call &lt;code&gt;OnNext&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Keep it private and expose &lt;code&gt;Observable&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;static/global &lt;code&gt;Subject&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Publisher, subscriber, and lifetime become unclear&lt;/td&gt;
&lt;td&gt;Keep events within a feature or screen scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Subscribe&lt;/code&gt; without lifetime&lt;/td&gt;
&lt;td&gt;Leaks and duplicate execution&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;AddTo&lt;/code&gt; / &lt;code&gt;DisposableBag&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adding subscriptions every &lt;code&gt;Initialize()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Re-initialization creates duplicates&lt;/td&gt;
&lt;td&gt;Dispose binding disposables before rebinding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-button &lt;code&gt;Drop&lt;/code&gt; for exclusive actions&lt;/td&gt;
&lt;td&gt;Does not prevent simultaneous input&lt;/td&gt;
&lt;td&gt;Merge by mutual-exclusion group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Using &lt;code&gt;EveryUpdate&lt;/code&gt; as &lt;code&gt;Update&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Intent becomes harder to follow&lt;/td&gt;
&lt;td&gt;Use normal &lt;code&gt;Update&lt;/code&gt; for ordinary loops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Making everything &lt;code&gt;ToObservable()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Harder than LINQ or &lt;code&gt;foreach&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Use Observable only where time flow matters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Very long chains&lt;/td&gt;
&lt;td&gt;Hard to review&lt;/td&gt;
&lt;td&gt;Split into intermediate Observables or methods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swallowing errors in &lt;code&gt;OnErrorResume&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Failures disappear from monitoring&lt;/td&gt;
&lt;td&gt;Define logging and display rules&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Even when exposing &lt;code&gt;ReadOnlyReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt; or &lt;code&gt;Observable&amp;lt;T&amp;gt;&lt;/code&gt;, a caller who knows the concrete object could force an unsafe cast. The point is not to make abuse impossible through types alone. The point is to hide write access from the public API and make unsafe casts a team-rule violation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Team operation rules
&lt;/h2&gt;

&lt;p&gt;Based on the table above, code review can focus on four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the write entry point hidden?&lt;/li&gt;
&lt;li&gt;Is the subscription lifetime clear, including re-initialization?&lt;/li&gt;
&lt;li&gt;Are repeated clicks, simultaneous input, and one-shot control treated as separate problems?&lt;/li&gt;
&lt;li&gt;Is the code using Observable where ordinary C#, LINQ, &lt;code&gt;Update&lt;/code&gt;, or &lt;code&gt;async/await&lt;/code&gt; would be clearer?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not start by creating too many detailed rules. It is safer to first establish private write access, lifetime management, input exclusivity, and limited &lt;code&gt;EveryUpdate&lt;/code&gt; usage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adoption scope
&lt;/h2&gt;

&lt;p&gt;R3 is safer to introduce gradually, not all at once across the entire project.&lt;/p&gt;

&lt;p&gt;A practical adoption order is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;UI events and repeated-click protection&lt;/li&gt;
&lt;li&gt;ViewModel-to-View state notification&lt;/li&gt;
&lt;li&gt;Loading progress and busy flags&lt;/li&gt;
&lt;li&gt;Composition of multiple inputs&lt;/li&gt;
&lt;li&gt;Game events only where needed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you try to convert every Model, Service, and event to R3 at once, review cost will rise quickly. In a team with R3 beginners, starting from UI and ViewModel code usually leads to smoother adoption.&lt;/p&gt;




&lt;h2&gt;
  
  
  Notes for migrating from UniRx
&lt;/h2&gt;

&lt;p&gt;If your team has UniRx experience, people may treat R3 as just a newer UniRx. But several design details are different.&lt;/p&gt;

&lt;p&gt;Important points include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;Debounce&lt;/code&gt;, not &lt;code&gt;Throttle&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Do not assume &lt;code&gt;OnError&lt;/code&gt; stops the subscription.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;SubscribeAwait&lt;/code&gt; and &lt;code&gt;AwaitOperation&lt;/code&gt; for async UI events.&lt;/li&gt;
&lt;li&gt;Follow R3's disposable patterns such as &lt;code&gt;DisposableBag&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Be explicit about Unity providers where timing matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Migration is not only API replacement. Revisit error handling, async behavior, and subscription lifetime design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recommended practical structure
&lt;/h2&gt;

&lt;p&gt;A typical maintainable structure is to separate Model, Presenter/ViewModel, and View responsibilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model
&lt;/h3&gt;

&lt;p&gt;The Model owns state and publishes change notifications. It does not expose write access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerStatus&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDisposable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_hp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ReadOnlyReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_hp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;_hp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_hp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_hp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ViewModel / Presenter
&lt;/h3&gt;

&lt;p&gt;The ViewModel or Presenter converts Model state into UI-friendly state. Any generated &lt;code&gt;ReadOnlyReactiveProperty&lt;/code&gt; must also have a lifetime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;DisposableBag&lt;/span&gt; &lt;span class="n"&gt;_disposables&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;ReadOnlyReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_canDecide&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ReadOnlyReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CanDecide&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_canDecide&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Wallet&lt;/span&gt; &lt;span class="n"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_disposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_disposables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;_canDecide&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CombineLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Gold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;hp&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToReadOnlyReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;_canDecide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;_disposables&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_disposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;Initialize()&lt;/code&gt; may be called more than once, rebuild subscriptions after disposing the previous ones. If the class is single-initialization only, make that assumption explicit in the design.&lt;/p&gt;

&lt;h3&gt;
  
  
  View
&lt;/h3&gt;

&lt;p&gt;The View sends UI events to the Presenter and reflects Presenter state into UI.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerPresenter&lt;/span&gt; &lt;span class="n"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_bindDisposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_bindDisposables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CanDecide&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canDecide&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_decideButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interactable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;canDecide&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;_bindDisposables&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;_decideButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DecideAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;AwaitOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;_bindDisposables&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnDestroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_bindDisposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;Initialize()&lt;/code&gt; is guaranteed to be called only once, &lt;code&gt;AddTo(this)&lt;/code&gt; can be enough. If the View can be rebound or re-initialized, explicitly dispose the previous binding subscriptions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing not to use R3 is also important
&lt;/h2&gt;

&lt;p&gt;R3 is useful, but not every piece of code needs it.&lt;/p&gt;

&lt;p&gt;Examples where R3 is often unnecessary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple sequential logic&lt;/li&gt;
&lt;li&gt;One-time loading&lt;/li&gt;
&lt;li&gt;Complex damage calculation&lt;/li&gt;
&lt;li&gt;AI decision-making core&lt;/li&gt;
&lt;li&gt;Core physics update logic&lt;/li&gt;
&lt;li&gt;One-off collection aggregation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are often clearer with ordinary C#, &lt;code&gt;async/await&lt;/code&gt;, &lt;code&gt;Update&lt;/code&gt;, or LINQ.&lt;/p&gt;

&lt;p&gt;Use R3 where you need to compose events or value flows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI events&lt;/li&gt;
&lt;li&gt;State-change notifications&lt;/li&gt;
&lt;li&gt;Composition of multiple inputs&lt;/li&gt;
&lt;li&gt;Repeated-click and simultaneous-input protection&lt;/li&gt;
&lt;li&gt;Loading progress&lt;/li&gt;
&lt;li&gt;ViewModel-to-View notification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The purpose of R3 is not to make code look advanced. It is to make event flow and lifetime explicit enough for the team to maintain.&lt;/p&gt;




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

&lt;p&gt;R3 is very effective when its usage is constrained. It works well for UI events, ViewModel-to-View notifications, loading progress, busy flags, and input composition. In those areas, it can express intent more clearly than hand-written event management.&lt;/p&gt;

&lt;p&gt;But if the team uses it freely everywhere, &lt;code&gt;Subject&lt;/code&gt;, &lt;code&gt;EveryUpdate&lt;/code&gt;, long chains, and subscriptions with unclear lifetimes will multiply.&lt;/p&gt;

&lt;p&gt;The most important point in this article is to separate repeated clicks, simultaneous input, one-shot control, subscription lifetime, and time providers. For a 10-person Unity team that needs to maintain a project over time, R3 should not be introduced as a tool for making everything Observable. It should be introduced as a &lt;strong&gt;restricted set of rules for writing notification and wiring code safely&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>gamedev</category>
      <category>programming</category>
    </item>
    <item>
      <title>R3 Design Rules for Unity 6: Keeping Reactive Code Maintainable in a 10-Person Team</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Mon, 06 Jul 2026 16:09:43 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/r3-design-rules-for-unity-6-keeping-reactive-code-maintainable-in-a-10-person-team-2ik</link>
      <guid>https://dev.to/gamedevtoollab/r3-design-rules-for-unity-6-keeping-reactive-code-maintainable-in-a-10-person-team-2ik</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;There are many ways to handle events and state changes in Unity.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Update&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UnityEvent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;C# events&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Action&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;async/await&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;UniTask&lt;/li&gt;
&lt;li&gt;UniRx&lt;/li&gt;
&lt;li&gt;R3&lt;/li&gt;
&lt;li&gt;custom event buses&lt;/li&gt;
&lt;li&gt;ScriptableObject-based events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a small prototype, almost any of these can work.&lt;/p&gt;

&lt;p&gt;The situation changes when a project has around ten programmers and multiple people touch the same UI, game flow, networking, effects, and loading code. Event handling quickly becomes one of the easiest places to lose control.&lt;/p&gt;

&lt;p&gt;Typical problems look like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nobody knows where a subscription is created.&lt;/li&gt;
&lt;li&gt;Nobody knows when a subscription is disposed.&lt;/li&gt;
&lt;li&gt;Events are delivered to destroyed GameObjects.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Subject&lt;/code&gt; turns into a global event bus.&lt;/li&gt;
&lt;li&gt;UI updates are scattered across many classes.&lt;/li&gt;
&lt;li&gt;async code and input-spam prevention are mixed together.&lt;/li&gt;
&lt;li&gt;leaked subscriptions cause memory leaks or duplicated execution.&lt;/li&gt;
&lt;li&gt;only the people who already know Rx can read the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;R3 is a modern Reactive Extensions implementation developed by Cysharp.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/Cysharp/R3" rel="noopener noreferrer"&gt;Cysharp/R3 README&lt;/a&gt; describes R3 as a new future for &lt;code&gt;dotnet/reactive&lt;/code&gt; and UniRx, with support for multiple platforms including Unity. The author also introduces R3 as a modern reimplementation of Reactive Extensions for C# in &lt;a href="https://neue.cc/2024/02/27_R3.html" rel="noopener noreferrer"&gt;this article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For Unity projects, you usually install both the core &lt;code&gt;R3&lt;/code&gt; package and &lt;code&gt;R3.Unity&lt;/code&gt;. Unity support starts from Unity 2021.3.&lt;/p&gt;

&lt;p&gt;In production, keep the versions of &lt;code&gt;R3&lt;/code&gt; and &lt;code&gt;R3.Unity&lt;/code&gt; aligned. Updating only one side, or leaving one side behind, can cause API mismatches or dependency trouble. It is better to define the installation and update procedure as part of the project rules.&lt;/p&gt;

&lt;p&gt;In Unity, &lt;code&gt;UnityTimeProvider.Update&lt;/code&gt; and &lt;code&gt;UnityFrameProvider.Update&lt;/code&gt; are set as defaults, so R3 fits naturally into Unity 6-era projects.&lt;/p&gt;

&lt;p&gt;This article focuses on how to use R3 in a maintainable way in Unity 6 projects, and which patterns should be avoided in team development.&lt;/p&gt;

&lt;p&gt;The target environment is roughly this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unity 6 series&lt;/li&gt;
&lt;li&gt;around ten programmers&lt;/li&gt;
&lt;li&gt;long-term operation and maintenance&lt;/li&gt;
&lt;li&gt;UI, loading, networking, game flow, and effects touched by multiple people&lt;/li&gt;
&lt;li&gt;a mix of UniRx users and people new to reactive programming&lt;/li&gt;
&lt;li&gt;a need to care about GC, subscription leaks, and operational stability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a full introduction to every R3 feature.&lt;/p&gt;

&lt;p&gt;The goal is simpler: &lt;strong&gt;define practical rules that prevent R3 from breaking maintainability in team development&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Code samples omit common &lt;code&gt;using&lt;/code&gt; directives such as &lt;code&gt;using R3;&lt;/code&gt;, &lt;code&gt;using UnityEngine;&lt;/code&gt;, R3.Unity uGUI extensions, TextMeshPro namespaces, and &lt;code&gt;CancellationToken&lt;/code&gt;. Adjust them to match your UI package, DI container, and project structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;If you use R3 in Unity, start with these rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use R3 for event wiring and state-change notification.&lt;/li&gt;
&lt;li&gt;Leave one-shot async operations to &lt;code&gt;async/await&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Expose &lt;code&gt;Observable&amp;lt;T&amp;gt;&lt;/code&gt; or &lt;code&gt;ReadOnlyReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;Subject&amp;lt;T&amp;gt;&lt;/code&gt; and writable &lt;code&gt;ReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt; private.&lt;/li&gt;
&lt;li&gt;Always tie subscriptions to a GameObject, Component, ViewModel, or another explicit owner.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;SubscribeAwait&lt;/code&gt; and &lt;code&gt;AwaitOperation&lt;/code&gt; for UI input control in the operational layer.&lt;/li&gt;
&lt;li&gt;Use frame-based operators such as &lt;code&gt;IntervalFrame&lt;/code&gt;, &lt;code&gt;DelayFrame&lt;/code&gt;, and &lt;code&gt;EveryUpdate&lt;/code&gt; only when the frame semantics matter.&lt;/li&gt;
&lt;li&gt;Limit the R3 patterns allowed in the team.&lt;/li&gt;
&lt;li&gt;Do not turn everything into an Observable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most important point is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not turn R3 into a magical application-wide event system.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;R3 is powerful. If everyone uses it freely, the project can quickly become a codebase where nobody knows where events come from or who reacts to them.&lt;/p&gt;

&lt;p&gt;For maintainability, use R3 as a strong tool, but keep its scope controlled.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where R3 works well
&lt;/h2&gt;

&lt;p&gt;R3 is a good fit when you want to compose a stream of values or events.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;button clicks&lt;/li&gt;
&lt;li&gt;Toggle and Slider changes&lt;/li&gt;
&lt;li&gt;input events&lt;/li&gt;
&lt;li&gt;HP, stamina, currency, and other state-change notifications&lt;/li&gt;
&lt;li&gt;ViewModel-to-View updates&lt;/li&gt;
&lt;li&gt;loading progress&lt;/li&gt;
&lt;li&gt;busy/loading flags&lt;/li&gt;
&lt;li&gt;input-spam prevention&lt;/li&gt;
&lt;li&gt;delayed-by-frame behavior&lt;/li&gt;
&lt;li&gt;combining multiple events&lt;/li&gt;
&lt;li&gt;updating UI only when a value changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the other hand, not everything should be pushed into R3.&lt;/p&gt;

&lt;p&gt;These are usually better left outside R3:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simple sequential processing&lt;/li&gt;
&lt;li&gt;one-shot loading&lt;/li&gt;
&lt;li&gt;complex domain logic&lt;/li&gt;
&lt;li&gt;cutscene or effect sequences that are easier to read procedurally&lt;/li&gt;
&lt;li&gt;large amounts of per-frame logic&lt;/li&gt;
&lt;li&gt;the core of physics updates&lt;/li&gt;
&lt;li&gt;AI decision-making logic&lt;/li&gt;
&lt;li&gt;battle calculation logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;R3 is for observing, transforming, combining, and subscribing to value streams.&lt;/p&gt;

&lt;p&gt;You do not need to convert every piece of code into an Observable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Separate R3 from async/await
&lt;/h2&gt;

&lt;p&gt;The first rule to decide is how R3 and &lt;code&gt;async/await&lt;/code&gt; should share responsibility.&lt;/p&gt;

&lt;p&gt;The Cysharp/R3 README follows the idea that one-shot asynchronous operations should generally be represented by &lt;code&gt;***Async&lt;/code&gt; methods returning &lt;code&gt;Task&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In Unity projects, I recommend this split.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use async/await for these
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;loading master data once&lt;/li&gt;
&lt;li&gt;loading one prefab through Addressables&lt;/li&gt;
&lt;li&gt;calling one API endpoint&lt;/li&gt;
&lt;li&gt;saving data once&lt;/li&gt;
&lt;li&gt;running initialization steps in order&lt;/li&gt;
&lt;li&gt;showing one dialog and waiting for its result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When using Addressables, waiting with &lt;code&gt;async/await&lt;/code&gt; and releasing resources are separate responsibilities. The owner of the loaded resource still needs to own &lt;code&gt;Addressables.Release&lt;/code&gt;, &lt;code&gt;Addressables.ReleaseInstance&lt;/code&gt;, or the equivalent cleanup. This is similar to R3 subscription lifetime management: do not make ownership vague.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use R3 for these
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;handling repeated button clicks&lt;/li&gt;
&lt;li&gt;reflecting value changes into the UI&lt;/li&gt;
&lt;li&gt;deriving button interactability from multiple states&lt;/li&gt;
&lt;li&gt;showing and hiding loading UI from a busy flag&lt;/li&gt;
&lt;li&gt;controlling repeated input with &lt;code&gt;Drop&lt;/code&gt; or &lt;code&gt;Switch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;handling events based on frames, timers, or repeated intervals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, a loading operation itself can be plain &lt;code&gt;async/await&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PlayerMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;LoadPlayerMasterAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadTextAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"player_master.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;JsonUtility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromJson&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PlayerMaster&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This short example uses &lt;code&gt;JsonUtility&lt;/code&gt; only to keep the sample simple. In real projects, choose a JSON library or custom loader based on your data format. &lt;code&gt;JsonUtility&lt;/code&gt; has limitations such as dictionaries, top-level arrays, and null handling.&lt;/p&gt;

&lt;p&gt;If you want to expose loading progress to the UI, R3 becomes useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoadingModel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDisposable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_progress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ReadOnlyReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Progress&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_progress&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;SetProgress&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;_progress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_progress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A class like &lt;code&gt;LoadingModel&lt;/code&gt; implements &lt;code&gt;IDisposable&lt;/code&gt;, so whoever creates it must also define when it is disposed. That owner might be a DI container, a LifetimeScope, a screen Presenter, or a MonoBehaviour.&lt;/p&gt;

&lt;p&gt;The operation itself is &lt;code&gt;async/await&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The state notification is R3.&lt;/p&gt;

&lt;p&gt;This separation makes the code much easier to read in a team.&lt;/p&gt;




&lt;h2&gt;
  
  
  Publish R3 events on the Unity main thread by default
&lt;/h2&gt;

&lt;p&gt;In Unity projects, it is safest to make this a team rule:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Subject.OnNext&lt;/code&gt; and &lt;code&gt;ReactiveProperty&amp;lt;T&amp;gt;.Value&lt;/code&gt; updates should normally happen on the Unity main thread.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Cysharp/R3 README explains that operator composition is designed to be thread-safe, but the values flowing through &lt;code&gt;OnNext&lt;/code&gt; are expected to be published from a single thread. If &lt;code&gt;Subject.OnNext&lt;/code&gt; is called from multiple threads, synchronization such as &lt;code&gt;lock&lt;/code&gt; is required. &lt;code&gt;ReactiveProperty&lt;/code&gt; also does not make simultaneous &lt;code&gt;OnNext&lt;/code&gt;, &lt;code&gt;Value&lt;/code&gt; updates, and &lt;code&gt;Subscribe&lt;/code&gt; operations thread-safe.&lt;/p&gt;

&lt;p&gt;A practical Unity rule looks 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;// Example team policy&lt;/span&gt;
&lt;span class="c1"&gt;// - Update ReactiveProperty.Value on the Unity main thread.&lt;/span&gt;
&lt;span class="c1"&gt;// - Do not call Subject.OnNext directly from external SDK callbacks or background threads.&lt;/span&gt;
&lt;span class="c1"&gt;// - If a result comes from Task.Run, a custom thread, or an SDK callback,&lt;/span&gt;
&lt;span class="c1"&gt;//   return to the main thread before notifying the UI or game layer when needed.&lt;/span&gt;
&lt;span class="c1"&gt;// - If multi-threaded publication is truly required, consider lock,&lt;/span&gt;
&lt;span class="c1"&gt;//   Synchronize, SynchronizedReactiveProperty, or a dedicated queue.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Network callbacks and external SDK events do not always return on Unity's main thread.&lt;/p&gt;

&lt;p&gt;This is not only about "UI must be updated on the main thread." It is also about keeping the R3 event stream itself on a predictable thread.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keep ReactiveProperty private
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt; is one of the most useful R3 types for state.&lt;/p&gt;

&lt;p&gt;It is also easy to misuse.&lt;/p&gt;

&lt;p&gt;If you expose it directly as public, any caller can write to it.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerStatus&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now any caller can do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;playerStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;999&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 convenient in the short term, but once the game is in operation, it becomes hard to answer a basic question: "Who changed HP?"&lt;/p&gt;

&lt;p&gt;For maintainability, keep writable &lt;code&gt;ReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt; fields private and expose them as read-only properties.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerStatus&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDisposable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_hp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ReadOnlyReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Hp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_hp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;_hp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_hp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Heal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;_hp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_hp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_hp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;PlayerStatus&lt;/code&gt; also implements &lt;code&gt;IDisposable&lt;/code&gt;, so the creator must dispose it. Tie it to the owner lifetime: screen, battle, scene, DI scope, or another explicit lifetime.&lt;/p&gt;

&lt;p&gt;External code can subscribe to HP changes, but normal API usage cannot modify the value directly.&lt;/p&gt;

&lt;p&gt;This is not perfect type-level security. Since the actual object is still a &lt;code&gt;ReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt;, an abusive downcast can still mutate it. Make it part of the team rule that &lt;code&gt;ReadOnlyReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt; must not be cast back to &lt;code&gt;ReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt; for mutation.&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;_playerStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_hpText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hp&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The basic pattern is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initialValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ReadOnlyReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_value&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 especially important in Models and ViewModels.&lt;/p&gt;

&lt;p&gt;R3 should make state flow easier to observe. It should not make state writable from anywhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  Be even more careful with Subject
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Subject&amp;lt;T&amp;gt;&lt;/code&gt; is convenient, but it is also one of the most dangerous R3 types.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;Subject&amp;lt;T&amp;gt;&lt;/code&gt; is an event publication endpoint. If it is public, anyone can call &lt;code&gt;OnNext&lt;/code&gt;.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="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;BattleEvents&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;Subject&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;OnDamage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now unrelated code can publish damage:&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;battleEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnDamage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;999&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That breaks the meaning of the event source.&lt;/p&gt;

&lt;p&gt;A more maintainable shape is to keep &lt;code&gt;Subject&amp;lt;T&amp;gt;&lt;/code&gt; private and expose it as &lt;code&gt;Observable&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BattleEvents&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDisposable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Subject&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;_damaged&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Damaged&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_damaged&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;PublishDamage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;_damaged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_damaged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the public API, other classes can treat it as subscription-only. Again, a type alone cannot prevent every abusive cast or rule violation. The important part is that the normal path does not expose the publication endpoint, and the team rule forbids casting it back to &lt;code&gt;Subject&lt;/code&gt; to publish events.&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;_battleEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Damaged&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ShowDamageEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now event publication is limited to the code path that can call &lt;code&gt;PublishDamage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That alone makes the event flow much easier to trace.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do not create a global Subject-based event bus
&lt;/h2&gt;

&lt;p&gt;A common failure after introducing R3 is a global event bus.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GlobalEventBus&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;readonly&lt;/span&gt; &lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or even with separate event types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GameEvents&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;readonly&lt;/span&gt; &lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PlayerDeadEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PlayerDead&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ItemGotEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ItemGot&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;QuestCompletedEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;QuestCompleted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can look useful in a small project.&lt;/p&gt;

&lt;p&gt;As the team grows, the problems appear quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;publishers are hard to trace&lt;/li&gt;
&lt;li&gt;subscribers are hard to trace&lt;/li&gt;
&lt;li&gt;event lifetime is vague&lt;/li&gt;
&lt;li&gt;subscriptions survive across scenes&lt;/li&gt;
&lt;li&gt;tests become harder&lt;/li&gt;
&lt;li&gt;event names keep increasing&lt;/li&gt;
&lt;li&gt;execution order becomes implicit&lt;/li&gt;
&lt;li&gt;the impact of specification changes becomes hard to predict&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a live game, someone may add an event later without noticing an existing subscriber, causing unexpected side effects.&lt;/p&gt;

&lt;p&gt;If you use R3, keep events scoped as much as possible.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;only inside Battle&lt;/li&gt;
&lt;li&gt;only inside one UI screen&lt;/li&gt;
&lt;li&gt;only inside a ViewModel&lt;/li&gt;
&lt;li&gt;only inside one Feature&lt;/li&gt;
&lt;li&gt;only inside a Scene or LifetimeScope&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an event truly needs to be application-wide, design it as an explicit application service or event aggregation layer. Do not just place an R3 &lt;code&gt;Subject&lt;/code&gt; globally and call it a day.&lt;/p&gt;




&lt;h2&gt;
  
  
  Every Subscribe must have a lifetime
&lt;/h2&gt;

&lt;p&gt;The most important part of R3 usage is subscription disposal.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Subscribe&lt;/code&gt; creates a subscription.&lt;/p&gt;

&lt;p&gt;If that subscription is never disposed, references can remain, events can be delivered to destroyed Views, and the same logic can run two or three times.&lt;/p&gt;

&lt;p&gt;The Cysharp/R3 README also emphasizes that subscription management is a core Rx concern, and poor management can lead to memory leaks.&lt;/p&gt;

&lt;p&gt;In Unity, the basic pattern is to tie subscriptions to a &lt;code&gt;Component&lt;/code&gt; or &lt;code&gt;GameObject&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OnClickAsObservable()&lt;/code&gt; and &lt;code&gt;OnValueChangedAsObservable()&lt;/code&gt; in these samples are R3.Unity uGUI extension methods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;OnClick&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This disposes the subscription according to the lifetime of this &lt;code&gt;MonoBehaviour&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can also tie it to a &lt;code&gt;GameObject&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_observable&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;blockquote&gt;
&lt;p&gt;If you call &lt;code&gt;Subscribe&lt;/code&gt;, decide the lifetime on the same line or nearby.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_playerStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_hpText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hp&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="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 code does not show when the subscription is disposed.&lt;/p&gt;

&lt;p&gt;It is also easy to miss in code review.&lt;/p&gt;

&lt;p&gt;Better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_playerStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_hpText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hp&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="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;R3.Unity's &lt;code&gt;AddTo(Component / GameObject)&lt;/code&gt; is designed to handle Unity-specific destruction issues by using &lt;code&gt;ObservableDestroyTrigger&lt;/code&gt;, including cases where inactive GameObjects can make destruction handling awkward.&lt;/p&gt;

&lt;p&gt;However, the lifetime here is basically &lt;strong&gt;destruction lifetime&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Do not assume &lt;code&gt;AddTo(this)&lt;/code&gt; means the subscription stops when the UI is hidden, disabled, or switched to another tab.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SetActive(false)&lt;/code&gt;, &lt;code&gt;OnDisable&lt;/code&gt;, tab changes, and internal screen state changes are different lifetimes. Decide per screen whether hidden UI should keep receiving model updates. If you need subscriptions to stop on disable or rebinding, manage that separately with another &lt;code&gt;DisposableBag&lt;/code&gt;, explicit re-subscription, &lt;code&gt;TakeUntil&lt;/code&gt;, &lt;code&gt;CancellationToken&lt;/code&gt;, or another lifecycle mechanism.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to choose disposable containers
&lt;/h2&gt;

&lt;p&gt;R3 gives you several ways to manage subscriptions.&lt;/p&gt;

&lt;p&gt;For maintainability, keep the team rule simple.&lt;/p&gt;

&lt;h3&gt;
  
  
  AddTo(this)
&lt;/h3&gt;

&lt;p&gt;Use this for the normal MonoBehaviour 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="n"&gt;_observable&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OnValueChanged&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should be the default in Unity Views and Presenters.&lt;/p&gt;

&lt;h3&gt;
  
  
  DisposableBag
&lt;/h3&gt;

&lt;p&gt;Use this when a normal C# class owns multiple subscriptions.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateHp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;_disposables&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mp&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateMp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;_disposables&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_disposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DisposableBag&lt;/code&gt; is a struct, so avoid accidental copies.&lt;/p&gt;

&lt;p&gt;Hold it as a field and pass it by &lt;code&gt;ref&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disposable.Combine / Builder
&lt;/h3&gt;

&lt;p&gt;When the number of subscriptions is fixed and small, you can build one disposable.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Disposable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateHp&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateMp&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;_disposable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_disposable&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the number of subscriptions is static, this keeps the code simple and avoids unnecessary dynamic management.&lt;/p&gt;

&lt;h3&gt;
  
  
  CompositeDisposable
&lt;/h3&gt;

&lt;p&gt;Use this only when you need dynamic add/remove behavior.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IDisposable&lt;/span&gt; &lt;span class="n"&gt;disposable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;disposable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_disposables&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_disposables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CompositeDisposable&lt;/code&gt; has more features, but that does not make it the default choice.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;AddTo(this)&lt;/code&gt; for MonoBehaviours.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DisposableBag&lt;/code&gt; for normal classes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Disposable.CreateBuilder()&lt;/code&gt; or &lt;code&gt;Disposable.Combine&lt;/code&gt; for a fixed small set.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CompositeDisposable&lt;/code&gt; only when removal or dynamic management is needed.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  R3 is especially useful for UI
&lt;/h2&gt;

&lt;p&gt;UI is one of the best places to use R3 in Unity.&lt;/p&gt;

&lt;p&gt;UI often needs to update when state changes.&lt;/p&gt;

&lt;p&gt;Here is a simple HP display:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerStatusView&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;TextMeshProUGUI&lt;/span&gt; &lt;span class="n"&gt;_hpText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Slider&lt;/span&gt; &lt;span class="n"&gt;_hpSlider&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;PlayerStatus&lt;/span&gt; &lt;span class="n"&gt;_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;Initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_status&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;_status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hp&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateHp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;UpdateHp&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;hp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_hpText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hp&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;_hpSlider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hp&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 much can also be done with a normal event.&lt;/p&gt;

&lt;p&gt;In this sample, &lt;code&gt;_hpSlider.value = hp&lt;/code&gt; assumes &lt;code&gt;Slider.maxValue&lt;/code&gt; matches the max HP. If your project uses normalized 0-1 slider values, convert the value in the View, such as &lt;code&gt;hp / maxHp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Also, this example assumes &lt;code&gt;Initialize()&lt;/code&gt; is called only once. If prefabs are reused, screens are re-initialized, DI injection happens again, or tabs are rebound, dispose the previous subscriptions before subscribing again. &lt;code&gt;AddTo(this)&lt;/code&gt; keeps subscriptions until the GameObject is destroyed, so it does not automatically remove subscriptions created by repeated initialization.&lt;/p&gt;

&lt;p&gt;R3 becomes more useful when UI state depends on multiple 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="n"&gt;_canDecide&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Observable&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CombineLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SelectedItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasEnoughGold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsBusy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hasGold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isBusy&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;item&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;hasGold&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;isBusy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToReadOnlyReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// If this MonoBehaviour owns it:&lt;/span&gt;
&lt;span class="n"&gt;_canDecide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// If a normal Presenter / ViewModel owns it:&lt;/span&gt;
&lt;span class="c1"&gt;// _canDecide.AddTo(ref _disposables);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ToReadOnlyReactiveProperty()&lt;/code&gt; creates an object that owns an upstream subscription. That generated &lt;code&gt;ReadOnlyReactiveProperty&lt;/code&gt; also needs its lifetime managed.&lt;/p&gt;

&lt;p&gt;In this sample, the MonoBehaviour owns &lt;code&gt;_canDecide&lt;/code&gt;, so it uses &lt;code&gt;AddTo(this)&lt;/code&gt;. If a Presenter or ViewModel owns it, tie it to that class's &lt;code&gt;DisposableBag&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;The button can subscribe to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_canDecide&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canDecide&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_decideButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interactable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;canDecide&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where R3 shines: deriving UI state from multiple model states.&lt;/p&gt;

&lt;p&gt;If the expression grows too long, move the logic into a named method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;CanDecide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt; &lt;span class="n"&gt;item&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;hasGold&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;isBusy&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;item&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;hasGold&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;isBusy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_canDecide&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Observable&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CombineLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SelectedItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasEnoughGold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsBusy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CanDecide&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToReadOnlyReactiveProperty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// If this MonoBehaviour owns it:&lt;/span&gt;
&lt;span class="n"&gt;_canDecide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// If a normal Presenter / ViewModel owns it:&lt;/span&gt;
&lt;span class="c1"&gt;// _canDecide.AddTo(ref _disposables);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;R3 chains are easy to read when they are short. When they become long, split them. The follow-up article covers more detailed rules for splitting complex chains.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do not put heavy logic inside Subscribe
&lt;/h2&gt;

&lt;p&gt;A common R3 mistake is putting too much logic inside &lt;code&gt;Subscribe&lt;/code&gt;.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SelectedItem&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;_dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Please select an item."&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;_wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasEnoughGold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SelectedItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Not enough gold."&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;_wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SelectedItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SelectedItem&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_sound&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decide&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"buy_item"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is easy to write at first, but it does not age well.&lt;/p&gt;

&lt;p&gt;Use the R3 chain as the event entry point.&lt;/p&gt;

&lt;p&gt;Move the actual logic to methods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnClickAsObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;TryBuySelectedItem&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;TryBuySelectedItem&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;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SelectedItem&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;_dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Please select an item."&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;_wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasEnoughGold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SelectedItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Not enough gold."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;Buy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SelectedItem&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;Buy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_sound&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decide&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"buy_item"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&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;R3 should express &lt;strong&gt;when&lt;/strong&gt; something is called.&lt;/p&gt;

&lt;p&gt;Game logic should remain in ordinary methods.&lt;/p&gt;

&lt;p&gt;That separation makes code review much easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This article covered the basic design rules for using R3 in Unity 6-era team development.&lt;/p&gt;

&lt;p&gt;The key idea is not to spread R3 everywhere just because it is convenient.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use R3 for event wiring and state-change notification.&lt;/li&gt;
&lt;li&gt;Leave one-shot async operations to &lt;code&gt;async/await&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;ReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt; and &lt;code&gt;Subject&amp;lt;T&amp;gt;&lt;/code&gt; private.&lt;/li&gt;
&lt;li&gt;Expose &lt;code&gt;ReadOnlyReactiveProperty&amp;lt;T&amp;gt;&lt;/code&gt; or &lt;code&gt;Observable&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Always decide the lifetime when you call &lt;code&gt;Subscribe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Remember that &lt;code&gt;AddTo(this)&lt;/code&gt; is destruction lifetime, not hidden/disabled lifetime.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;Initialize()&lt;/code&gt; can run multiple times, dispose previous subscriptions before subscribing again.&lt;/li&gt;
&lt;li&gt;Do not put heavy logic directly inside &lt;code&gt;Subscribe&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next article will cover the operational side: &lt;code&gt;SubscribeAwait&lt;/code&gt;, &lt;code&gt;AwaitOperation&lt;/code&gt;, &lt;code&gt;EveryUpdate&lt;/code&gt;, providers, Observable Tracker, forbidden patterns, and code review points.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>gamedev</category>
      <category>r3</category>
    </item>
    <item>
      <title>LINQ and ZLinq in the Unity 6 Era: Avoiding GC Allocations in Large-Scale Projects</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Fri, 03 Jul 2026 03:05:24 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/linq-and-zlinq-in-the-unity-6-era-avoiding-gc-allocations-in-large-scale-projects-36c2</link>
      <guid>https://dev.to/gamedevtoollab/linq-and-zlinq-in-the-unity-6-era-avoiding-gc-allocations-in-large-scale-projects-36c2</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In large-scale Unity development, GC Alloc can quietly become a real problem.&lt;/p&gt;

&lt;p&gt;At first, nothing looks wrong. But as the project grows and you add more enemies, UI, master data, events, states, notifications, logs, and other systems, small allocations that happen every frame begin to pile up.&lt;/p&gt;

&lt;p&gt;LINQ is especially convenient.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;aliveEnemies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAlive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;DistanceToPlayer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is readable.&lt;br&gt;
But if this kind of code runs every frame, it can become a source of both GC Alloc and CPU overhead.&lt;/p&gt;

&lt;p&gt;Unity's official documentation also recommends reducing frequent managed heap allocations as much as possible, ideally getting close to 0 bytes per frame.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.unity3d.com/2022.3/Documentation/Manual/performance-garbage-collection-best-practices.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/2022.3/Documentation/Manual/performance-garbage-collection-best-practices.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For general GC Alloc best practices, this article refers to the Unity 2022.3 documentation, because the general guidance still applies. Unity 6-specific GC behavior is covered later using the Unity 6.0 documentation.&lt;/p&gt;

&lt;p&gt;This article assumes Unity 6.0 as the minimum Unity version and explains how to choose between regular LINQ and ZLinq in production code.&lt;/p&gt;

&lt;p&gt;Unity 6.0 uses the Roslyn C# compiler, and its C# language version is C# 9.0. However, some C# 9 features, such as init-only setters, are not supported.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.unity3d.com/6000.0/Documentation/Manual/csharp-compiler.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/6000.0/Documentation/Manual/csharp-compiler.html&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;The point of this article is not to ban LINQ completely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not use LINQ in hot paths just because it is readable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not assume ZLinq solves everything just because you introduced it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those are the two main ideas.&lt;/p&gt;

&lt;p&gt;A rough guideline looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Guideline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Editor extensions, build scripts, debug code&lt;/td&gt;
&lt;td&gt;Regular LINQ is usually fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startup, loading, initialization&lt;/td&gt;
&lt;td&gt;LINQ can be fine, but measure when data size is large&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update / LateUpdate / FixedUpdate&lt;/td&gt;
&lt;td&gt;Avoid LINQ by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code that is not per-frame but still called frequently&lt;/td&gt;
&lt;td&gt;Consider ZLinq&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code that materializes results into List or Array&lt;/td&gt;
&lt;td&gt;Prefer reusing a preallocated List over LINQ/ZLinq&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Burst / Job / NativeArray-heavy code&lt;/td&gt;
&lt;td&gt;This article does not go deep into this area; prefer for loops and Native Collections&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Unity 6 GC assumptions
&lt;/h2&gt;

&lt;p&gt;In Unity 6, Incremental GC is enabled by default.&lt;br&gt;
Unity uses the Boehm-Demers-Weiser garbage collector. With Incremental GC, garbage collection work is split across multiple frames to reduce GC spikes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.unity3d.com/6000.0/Documentation/Manual/performance-incremental-garbage-collection.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/6000.0/Documentation/Manual/performance-incremental-garbage-collection.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, Incremental GC does not make garbage collection itself faster.&lt;br&gt;
It spreads the workload across multiple frames.&lt;/p&gt;

&lt;p&gt;Unity's GC is also non-compacting. Older Unity documentation explains that Unity's GC is non-generational, which means it is not good at efficiently handling small and frequent temporary allocations, such as those caused by boxing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.unity3d.com/2022.3/Documentation/Manual/performance-garbage-collection-best-practices.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/2022.3/Documentation/Manual/performance-garbage-collection-best-practices.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You also need to be careful about platform-specific differences.&lt;br&gt;
Unity 6.0's Web platform documentation explains that GC on Web platforms does not run incrementally over multiple frames like it does on other platforms. Instead, it runs once at the end of every frame.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.unity3d.com/6000.0/Documentation/Manual/webgl-technical-overview.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/6000.0/Documentation/Manual/webgl-technical-overview.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Incremental GC also uses write barriers to track reference changes. In code that changes a very large number of references, the overhead of those write barriers may matter.&lt;br&gt;
This article focuses on reducing GC Alloc, but the final decision should always be based on profiling a Player build on the target platform.&lt;/p&gt;

&lt;p&gt;In other words, even in Unity 6, the basic policy does not change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduce per-frame GC Alloc as much as possible&lt;/li&gt;
&lt;li&gt;Do not underestimate small allocations&lt;/li&gt;
&lt;li&gt;Avoid allocations in hot paths before GC spikes become a problem&lt;/li&gt;
&lt;li&gt;Confirm the result with the Profiler&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Where LINQ tends to become a problem
&lt;/h2&gt;

&lt;p&gt;LINQ is especially risky in code like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_enemies&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAlive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;DistanceToPlayer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Attack&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is easy to write, but if it runs every frame, it can become problematic.&lt;/p&gt;

&lt;p&gt;The issue is not only GC Alloc.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enumeration cost from &lt;code&gt;Where&lt;/code&gt; and &lt;code&gt;Select&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sorting cost from &lt;code&gt;OrderBy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Allocations from &lt;code&gt;ToList()&lt;/code&gt; and &lt;code&gt;ToArray()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Closures created by lambdas&lt;/li&gt;
&lt;li&gt;Enumeration through &lt;code&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Accumulation of small allocations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a large project, one instance may not matter. The problem is that similar code can appear in dozens of places.&lt;/p&gt;

&lt;p&gt;The result is often not "someone wrote terrible code".&lt;br&gt;
It is more like "everyone used a convenient pattern a little bit, and GC Alloc slowly grew everywhere".&lt;/p&gt;
&lt;h2&gt;
  
  
  A case where a for loop is better
&lt;/h2&gt;

&lt;p&gt;The previous code is safer as a simple for loop if it runs in a hot path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Enemy&lt;/span&gt; &lt;span class="nf"&gt;FindNearestAliveEnemy&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;Enemy&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Enemy&lt;/span&gt; &lt;span class="n"&gt;nearest&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="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;nearestDistance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;enemies&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;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Enemy&lt;/span&gt; &lt;span class="n"&gt;enemy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAlive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DistanceToPlayer&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;distance&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;nearestDistance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;nearest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;nearestDistance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;distance&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;return&lt;/span&gt; &lt;span class="n"&gt;nearest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is longer than LINQ.&lt;br&gt;
But what it does is explicit.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It does not create intermediate enumerables&lt;/li&gt;
&lt;li&gt;It does not sort&lt;/li&gt;
&lt;li&gt;It does not create a List&lt;/li&gt;
&lt;li&gt;It only searches for the minimum value that matches the condition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kind of small difference matters in code that runs every frame during gameplay.&lt;/p&gt;

&lt;p&gt;However, this applies when you only need the nearest single item.&lt;br&gt;
If you need a fully sorted result, such as a ranking display or a list that must be processed in nearest-first order later, &lt;code&gt;OrderBy&lt;/code&gt; itself is not wrong.&lt;/p&gt;

&lt;p&gt;Also, if &lt;code&gt;DistanceToPlayer&lt;/code&gt; calculates &lt;code&gt;Vector3.Distance&lt;/code&gt; or &lt;code&gt;magnitude&lt;/code&gt; every time in real code, the distance calculation itself may be expensive before LINQ or ZLinq even enters the picture.&lt;br&gt;
For nearest-neighbor checks, you may also want to compare squared distance and cache the player's position before the loop.&lt;br&gt;
In this article's examples, &lt;code&gt;DistanceToPlayer&lt;/code&gt; is treated as a lightweight value to keep the code simple.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is ZLinq?
&lt;/h2&gt;

&lt;p&gt;ZLinq is a zero-allocation-oriented LINQ library by Cysharp.&lt;/p&gt;

&lt;p&gt;It lets you write code that resembles regular LINQ, while using &lt;code&gt;AsValueEnumerable()&lt;/code&gt; to convert the source into a value-type-based enumerable. The goal is to reduce allocations caused by method chains.&lt;br&gt;
The ZLinq README explains that, unlike regular LINQ, adding more method chains does not easily increase allocations, that it is implemented with struct-based &lt;code&gt;ValueEnumerable&lt;/code&gt;, and that it generally has better base performance than regular LINQ.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Cysharp/ZLinq" rel="noopener noreferrer"&gt;https://github.com/Cysharp/ZLinq&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The basic form looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;ZLinq&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;UpdateEnemies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Enemy&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enemy&lt;/span&gt; &lt;span class="n"&gt;enemy&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsValueEnumerable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&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;IsAlive&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Tick&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;Compared with regular LINQ, &lt;code&gt;AsValueEnumerable()&lt;/code&gt; is added.&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;// Regular LINQ&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enemy&lt;/span&gt; &lt;span class="n"&gt;enemy&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&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;IsAlive&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Tick&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ZLinq&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enemy&lt;/span&gt; &lt;span class="n"&gt;enemy&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsValueEnumerable&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&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;IsAlive&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Tick&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 code looks similar, but the enumeration mechanism is different.&lt;/p&gt;

&lt;p&gt;However, the important point is that adding &lt;code&gt;AsValueEnumerable()&lt;/code&gt; does not magically make everything zero-allocation.&lt;br&gt;
If you call &lt;code&gt;ToList()&lt;/code&gt;, a List is still created. If a lambda captures an external variable, allocations related to closures can still occur.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where ZLinq is easy to use
&lt;/h2&gt;

&lt;p&gt;ZLinq works well in cases like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enemy&lt;/span&gt; &lt;span class="n"&gt;enemy&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_enemies&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsValueEnumerable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&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;IsAlive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsVisible&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UpdateMarker&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 kind of code is a good fit when you want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filter&lt;/li&gt;
&lt;li&gt;Transform&lt;/li&gt;
&lt;li&gt;Enumerate&lt;/li&gt;
&lt;li&gt;Use the result immediately&lt;/li&gt;
&lt;li&gt;Avoid &lt;code&gt;ToList()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In large projects, API boundaries often use types such as &lt;code&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/code&gt; or &lt;code&gt;IReadOnlyList&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;br&gt;
Abstract types give callers more flexibility, but in hot paths, the fact that the static type is abstract can make profiling and optimization harder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ApplyDamage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Enemy&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enemy&lt;/span&gt; &lt;span class="n"&gt;enemy&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enumeration through an abstract type like this can cause extra allocations depending on the implementation and how the code is written.&lt;/p&gt;

&lt;p&gt;ZLinq explains that even when &lt;code&gt;AsValueEnumerable()&lt;/code&gt; is used on &lt;code&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;, it may be able to reduce allocations if the actual object is an array or &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Cysharp/ZLinq#unity" rel="noopener noreferrer"&gt;https://github.com/Cysharp/ZLinq#unity&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;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ApplyDamage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Enemy&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enemy&lt;/span&gt; &lt;span class="n"&gt;enemy&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsValueEnumerable&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, this is not universal.&lt;br&gt;
The optimizations explicitly described in the README generally assume that the actual object is an array, &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;, or a similar supported collection.&lt;br&gt;
When going through an abstract type such as &lt;code&gt;IReadOnlyList&amp;lt;T&amp;gt;&lt;/code&gt;, the behavior can depend on the static type, the actual object, and the ZLinq version.&lt;br&gt;
In hot paths, either make the concrete type clear, such as &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;, an array, or a Native Collection, or always confirm GC Alloc in a Player build.&lt;/p&gt;
&lt;h2&gt;
  
  
  Things to avoid even with ZLinq
&lt;/h2&gt;

&lt;p&gt;Even when using ZLinq, code like this still needs care:&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;list&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_enemies&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsValueEnumerable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&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;IsAlive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&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;Where&lt;/code&gt; chain may avoid allocations, but &lt;code&gt;ToList()&lt;/code&gt; still creates a List.&lt;/p&gt;

&lt;p&gt;In other words, ZLinq is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A way to make LINQ-like processing closer to zero-allocation&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A way to erase all collection creation from the result&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ZLinq's &lt;code&gt;ValueEnumerable&lt;/code&gt; also cannot be passed everywhere in the same way as a regular &lt;code&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;br&gt;
If an existing API requires &lt;code&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;, if you want to store the query result in a field, or if you need to use it across &lt;code&gt;await&lt;/code&gt; or &lt;code&gt;yield&lt;/code&gt;, you will eventually need to choose something like &lt;code&gt;ToArray()&lt;/code&gt;, &lt;code&gt;ToList()&lt;/code&gt;, or &lt;code&gt;ToArrayPool()&lt;/code&gt;.&lt;br&gt;
At that point, allocation and lifetime management become relevant again.&lt;/p&gt;

&lt;p&gt;If you need a List result every frame, it is often safer to allocate the List ahead of time and reuse it.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CollectAliveEnemies&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;Enemy&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_aliveEnemies&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;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;enemies&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;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Enemy&lt;/span&gt; &lt;span class="n"&gt;enemy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAlive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_aliveEnemies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not fancy, but it is very strong in large-scale Unity development.&lt;/p&gt;

&lt;p&gt;Unity's official documentation also introduces the pattern of keeping and reusing a List instead of creating a new one every frame.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.unity3d.com/2022.3/Documentation/Manual/performance-garbage-collection-best-practices.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/2022.3/Documentation/Manual/performance-garbage-collection-best-practices.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ZLinq also provides APIs such as &lt;code&gt;CopyTo(List&amp;lt;T&amp;gt;)&lt;/code&gt; for copying results into an existing destination.&lt;br&gt;
&lt;code&gt;CopyTo(List&amp;lt;T&amp;gt;)&lt;/code&gt; clears the destination List before filling it, so be careful if you want to preserve the existing contents.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Cysharp/ZLinq#additional-operators" rel="noopener noreferrer"&gt;https://github.com/Cysharp/ZLinq#additional-operators&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That said, until everyone on the team understands the behavior, &lt;code&gt;Clear()&lt;/code&gt; + &lt;code&gt;Add()&lt;/code&gt; is often easier to review for hot-path result collection.&lt;/p&gt;

&lt;p&gt;Reusable Lists are effective for reducing GC Alloc, but &lt;code&gt;Clear()&lt;/code&gt; usually keeps the internal array capacity.&lt;br&gt;
If a List grows extremely large only at peak moments, consider destroying or shrinking it at scene transitions or loading boundaries.&lt;br&gt;
Also, if you let another system keep a reference to a reusable List, the next &lt;code&gt;Clear()&lt;/code&gt; will change its contents.&lt;br&gt;
When passing such Lists to events, async code, or UI binding, define ownership and lifetime clearly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Watch out for closures
&lt;/h2&gt;

&lt;p&gt;With both LINQ and ZLinq, lambdas can create closures when they capture variables from outside the lambda.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;UpdateTargets&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;range&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enemy&lt;/span&gt; &lt;span class="n"&gt;enemy&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_enemies&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsValueEnumerable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;DistanceToPlayer&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetTarget&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;Here, &lt;code&gt;range&lt;/code&gt; is a variable outside the lambda.&lt;br&gt;
Even when using ZLinq, this kind of capture may cause the compiler to generate a closure object or delegate.&lt;br&gt;
In other words, even if the ZLinq enumeration itself is close to zero-allocation, the lambda can still cause GC Alloc.&lt;/p&gt;

&lt;p&gt;Unity's documentation also recommends minimizing closures and anonymous methods in performance-sensitive code, especially in code that runs every frame.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.unity3d.com/2022.3/Documentation/Manual/performance-garbage-collection-best-practices.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/2022.3/Documentation/Manual/performance-garbage-collection-best-practices.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When no capture is needed, use a &lt;code&gt;static&lt;/code&gt; lambda.&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;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enemy&lt;/span&gt; &lt;span class="n"&gt;enemy&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_enemies&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsValueEnumerable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&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;IsAlive&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Tick&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;Adding &lt;code&gt;static&lt;/code&gt; prevents the lambda from capturing variables outside itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;range&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="c1"&gt;// This captures range, so it cannot be static.&lt;/span&gt;
&lt;span class="c1"&gt;// .Where(static x =&amp;gt; x.DistanceToPlayer &amp;lt; range)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In hot paths, either shape the code so that &lt;code&gt;static&lt;/code&gt; lambdas can be used, or just write a for loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be careful with Drop-in Generator
&lt;/h2&gt;

&lt;p&gt;ZLinq also has a Drop-in Generator that redirects regular LINQ calls toward ZLinq.&lt;/p&gt;

&lt;p&gt;It is convenient, but in large projects, applying it broadly from the beginning is risky.&lt;/p&gt;

&lt;p&gt;The ZLinq README explains that Drop-in Generator uses a Source Generator to generate extension methods for each type, and that these extension methods take priority over regular LINQ.&lt;br&gt;
It also explains that, when using Drop-in Generator in Unity, the minimum Unity version is 2022.3.12f1 because of C# Incremental Source Generator support.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Cysharp/ZLinq#drop-in-replacement" rel="noopener noreferrer"&gt;https://github.com/Cysharp/ZLinq#drop-in-replacement&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Cysharp/ZLinq#unity" rel="noopener noreferrer"&gt;https://github.com/Cysharp/ZLinq#unity&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since this article assumes Unity 6.0, the version requirement is usually not a problem. But if you apply it to the whole project at once, the following issues can happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It becomes harder to understand the difference from regular LINQ&lt;/li&gt;
&lt;li&gt;It becomes harder to tell where ZLinq is being used&lt;/li&gt;
&lt;li&gt;It spreads before the whole team understands the behavior&lt;/li&gt;
&lt;li&gt;API boundaries that take &lt;code&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/code&gt; become harder to reason about&lt;/li&gt;
&lt;li&gt;The impact of dependency updates becomes broader&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first, I prefer writing &lt;code&gt;AsValueEnumerable()&lt;/code&gt; explicitly.&lt;br&gt;
That way, code review makes it clear that the code is intentionally using ZLinq.&lt;/p&gt;

&lt;p&gt;When installing ZLinq through tools such as NuGetForUnity, it is safer to pin the versions of ZLinq itself and Drop-in Generator.&lt;br&gt;
In team development, if dependency versions differ between developers, the assumptions used during review and benchmarking can also differ.&lt;/p&gt;

&lt;p&gt;If you use Drop-in Generator, asmdef and namespace boundaries should also be part of the team rules.&lt;br&gt;
Make it clear in which assemblies ZLinq should take priority over regular LINQ, and avoid accidentally applying it to third-party code or shared libraries.&lt;br&gt;
Some places may also change return types or available APIs compared with &lt;code&gt;System.Linq&lt;/code&gt;, so keep migration PRs small and check both profiler results and code diffs step by step.&lt;/p&gt;
&lt;h2&gt;
  
  
  Installing ZLinq in Unity
&lt;/h2&gt;

&lt;p&gt;When using ZLinq in Unity, the README describes a flow where the main ZLinq package is installed through NuGetForUnity or a similar tool, while Unity-specific features are referenced through the &lt;code&gt;ZLinq.Unity&lt;/code&gt; package via a Git URL.&lt;br&gt;
The Unity package adds support for GameObject and Transform hierarchy traversal, among other things.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Cysharp/ZLinq#unity" rel="noopener noreferrer"&gt;https://github.com/Cysharp/ZLinq#unity&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When referencing a package through a Git URL, specify a tag or commit hash when possible, and manage both &lt;code&gt;Packages/manifest.json&lt;/code&gt; and &lt;code&gt;packages-lock.json&lt;/code&gt; as a team.&lt;br&gt;
If only the NuGet-side ZLinq package is pinned while &lt;code&gt;ZLinq.Unity&lt;/code&gt; or &lt;code&gt;ZLinq.DropInGenerator&lt;/code&gt; is floating, behavior may differ subtly between developers or CI environments.&lt;/p&gt;

&lt;p&gt;For example, you can traverse a Transform hierarchy.&lt;br&gt;
The following code is intended as a startup or verification example. It is not meant to be moved directly into &lt;code&gt;Update()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;ZLinq&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;TransformSearchSample&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Transform&lt;/span&gt; &lt;span class="n"&gt;_root&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="k"&gt;foreach&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;child&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_root&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Descendants&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&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;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activeSelf&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="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transform hierarchy traversal is convenient, but if you run it every frame against a deep hierarchy, the traversal cost itself can become a problem.&lt;/p&gt;

&lt;p&gt;ZLinq does not make repeated hierarchy searches free.&lt;br&gt;
If the search target is large, consider caching or updating by differences.&lt;br&gt;
Also, &lt;code&gt;Debug.Log&lt;/code&gt; itself is expensive at runtime, so do not put it in hot paths.&lt;/p&gt;
&lt;h2&gt;
  
  
  Treat NativeArray / Job / Burst as a separate topic
&lt;/h2&gt;

&lt;p&gt;ZLinq supports Unity types such as &lt;code&gt;NativeArray&lt;/code&gt; and &lt;code&gt;NativeSlice&lt;/code&gt;.&lt;br&gt;
The README also explains that with Unity Collections 2.1.1 or later, more Native Collections are supported.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Cysharp/ZLinq#unity" rel="noopener noreferrer"&gt;https://github.com/Cysharp/ZLinq#unity&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, in truly low-level hot paths that use Jobs or Burst, data layout, Native Collections, and Burst compilation constraints often matter more than preserving a LINQ-like style.&lt;/p&gt;

&lt;p&gt;So this article focuses on how to handle LINQ and ZLinq in ordinary Unity runtime code, rather than on whether ZLinq can be used with &lt;code&gt;NativeArray&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Do not expect too much from SIMD in Unity
&lt;/h2&gt;

&lt;p&gt;ZLinq has SIMD-related features, but the README explains that SIMD is not used in Unity because Unity references .NET Standard 2.1.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Cysharp/ZLinq#unity" rel="noopener noreferrer"&gt;https://github.com/Cysharp/ZLinq#unity&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For Unity articles, it is less misleading to describe ZLinq as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A way to keep a LINQ-like style while reducing GC Alloc and often improving runtime performance compared with regular LINQ&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;rather than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A magic SIMD-based speedup tool&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;However, if you are aiming for maximum performance, that is a different discussion.&lt;br&gt;
The ZLinq README also explains that if you want the best possible performance, you should write the code inline.&lt;br&gt;
For hot paths, measure ZLinq and for loops with real data.&lt;/p&gt;
&lt;h2&gt;
  
  
  Be careful about code size with IL2CPP / AOT
&lt;/h2&gt;

&lt;p&gt;In large production titles, you need to check not only Mono execution in the Editor, but also IL2CPP builds.&lt;/p&gt;

&lt;p&gt;ZLinq uses structs and generics heavily.&lt;br&gt;
This can be beneficial for reducing GC Alloc and improving runtime performance, but depending on how it is used, it may affect generated IL2CPP code size, build time, and binary size.&lt;/p&gt;

&lt;p&gt;Unity's IL2CPP has generic sharing to reduce code size, but value-type generics may be less shareable in some cases.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://unity.com/blog/engine-platform/il2cpp-internals-generic-sharing-implementation" rel="noopener noreferrer"&gt;https://unity.com/blog/engine-platform/il2cpp-internals-generic-sharing-implementation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So when introducing ZLinq, it is safer to first apply it explicitly to the code that actually has a problem, instead of immediately applying Drop-in Generator to the whole project.&lt;br&gt;
Check the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GC Alloc in the Editor&lt;/li&gt;
&lt;li&gt;Profiler results in a Development Build&lt;/li&gt;
&lt;li&gt;IL2CPP build time&lt;/li&gt;
&lt;li&gt;Player size&lt;/li&gt;
&lt;li&gt;Runtime performance on the target platform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ZLinq is useful, but the wider the introduction scope becomes, the more you need to check its impact on the build output.&lt;/p&gt;
&lt;h2&gt;
  
  
  Minimum profiling conditions
&lt;/h2&gt;

&lt;p&gt;This article repeatedly says "check it with the Profiler", but it is important not to decide based only on Editor results.&lt;br&gt;
Especially in production titles that use IL2CPP, GC Alloc and CPU time in the Mono Editor are not enough for the final decision.&lt;/p&gt;

&lt;p&gt;At minimum, separate the following checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the Editor Profiler first to find where GC Alloc occurs&lt;/li&gt;
&lt;li&gt;Check the target platform Player with a Development Build&lt;/li&gt;
&lt;li&gt;If using IL2CPP, check CPU time, GC Alloc, Player size, and build time in an IL2CPP build&lt;/li&gt;
&lt;li&gt;Deep Profile is useful, but it has large overhead, so do not use it blindly for final numeric judgment&lt;/li&gt;
&lt;li&gt;Compare regular LINQ, ZLinq, and for loops with the same data size and call count&lt;/li&gt;
&lt;li&gt;When introducing Drop-in Generator, compare profiler results, Player size, and build time before and after the change, ideally per PR&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also, even when GC Alloc becomes 0, CPU cost does not disappear.&lt;br&gt;
Sorting with &lt;code&gt;OrderBy&lt;/code&gt;, Transform hierarchy traversal, distance calculation, virtual calls, cache misses, and other costs remain.&lt;br&gt;
ZLinq is a powerful option for reducing GC Alloc, but in hot paths, CPU time must be checked as well.&lt;/p&gt;
&lt;h2&gt;
  
  
  Suggested team rules
&lt;/h2&gt;

&lt;p&gt;In large-scale development, rules matter more than individual discipline.&lt;/p&gt;

&lt;p&gt;For example, the following rules are easy to review.&lt;/p&gt;
&lt;h3&gt;
  
  
  Places where LINQ is acceptable
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Editor extensions&lt;/li&gt;
&lt;li&gt;Importers&lt;/li&gt;
&lt;li&gt;Build scripts&lt;/li&gt;
&lt;li&gt;Debug code&lt;/li&gt;
&lt;li&gt;Test code&lt;/li&gt;
&lt;li&gt;One-time conversion at startup&lt;/li&gt;
&lt;li&gt;Temporary processing during loading&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Runtime areas that require confirmation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Update&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LateUpdate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FixedUpdate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Per-frame UI updates&lt;/li&gt;
&lt;li&gt;Updates for many enemies, bullets, effects, and similar objects&lt;/li&gt;
&lt;li&gt;Scroll list rebuilding&lt;/li&gt;
&lt;li&gt;Input handling&lt;/li&gt;
&lt;li&gt;Camera control&lt;/li&gt;
&lt;li&gt;State machines that run every frame&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If LINQ or ZLinq is used in these areas, the author should be able to explain during review why it is acceptable.&lt;/p&gt;
&lt;h3&gt;
  
  
  Places where ZLinq is worth considering
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Code that is not per-frame but is still called frequently&lt;/li&gt;
&lt;li&gt;Code where filtering or transformation should remain readable&lt;/li&gt;
&lt;li&gt;Code that enumerates results immediately without materializing a List&lt;/li&gt;
&lt;li&gt;Existing codebases full of LINQ that need gradual GC reduction&lt;/li&gt;
&lt;li&gt;Search code shared between Editor and Runtime&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Places where for loops should be preferred
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Truly hot code&lt;/li&gt;
&lt;li&gt;Code called thousands of times per frame&lt;/li&gt;
&lt;li&gt;Code that needs early &lt;code&gt;break&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Code that only needs the minimum, maximum, or first matching item&lt;/li&gt;
&lt;li&gt;Code that reuses result Lists&lt;/li&gt;
&lt;li&gt;Code strongly tied to Job / Burst / Native Collections&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Code review checklist
&lt;/h2&gt;

&lt;p&gt;In code review, it is more practical to ask the following questions than to reject every use of LINQ mechanically.&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;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(...).&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(...).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you see code like this, check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is it called every frame?&lt;/li&gt;
&lt;li&gt;Is the number of items large?&lt;/li&gt;
&lt;li&gt;Is &lt;code&gt;ToList()&lt;/code&gt; or &lt;code&gt;ToArray()&lt;/code&gt; really necessary?&lt;/li&gt;
&lt;li&gt;Is a closure being created?&lt;/li&gt;
&lt;li&gt;Is &lt;code&gt;OrderBy&lt;/code&gt; really necessary?&lt;/li&gt;
&lt;li&gt;Is it sorting only to call &lt;code&gt;FirstOrDefault&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Can the result List be reused?&lt;/li&gt;
&lt;li&gt;Can ZLinq preserve readability while reducing allocations?&lt;/li&gt;
&lt;li&gt;Would a for loop be clearer?&lt;/li&gt;
&lt;li&gt;Is the ownership and lifetime of a reused List clear?&lt;/li&gt;
&lt;li&gt;Do we need to check the impact on IL2CPP builds?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;OrderBy().FirstOrDefault()&lt;/code&gt; deserves particular attention.&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;target&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&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;IsAlive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&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;DistanceToPlayer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only need the minimum value, you do not need to sort.&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;Enemy&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="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;minDistance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;enemies&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;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Enemy&lt;/span&gt; &lt;span class="n"&gt;enemy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAlive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DistanceToPlayer&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;distance&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;minDistance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;minDistance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this kind of case, changing the algorithm is more effective than replacing LINQ with ZLinq.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;LINQ and ZLinq are both useful.&lt;br&gt;
But in large-scale Unity development, if you do not decide where they are allowed, convenient code can directly turn into runtime performance problems.&lt;/p&gt;

&lt;p&gt;The policy in this article is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid LINQ in hot paths&lt;/li&gt;
&lt;li&gt;Consider ZLinq for frequently called code where readability still matters&lt;/li&gt;
&lt;li&gt;Be suspicious of &lt;code&gt;ToList()&lt;/code&gt;, &lt;code&gt;ToArray()&lt;/code&gt;, closures, and unnecessary &lt;code&gt;OrderBy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Keep the ZLinq introduction scope controlled, and verify with the Profiler and IL2CPP builds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ZLinq is not a free pass to use LINQ carelessly.&lt;/p&gt;

&lt;p&gt;It is best treated as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An option for keeping LINQ readability while still thinking seriously about GC&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>performance</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>A Deep Dive into C# Dispose: GC, Finalizers, and UnityEngine.Object Destruction</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Wed, 01 Jul 2026 03:00:23 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/a-deep-dive-into-c-dispose-gc-finalizers-and-unityengineobject-destruction-3175</link>
      <guid>https://dev.to/gamedevtoollab/a-deep-dive-into-c-dispose-gc-finalizers-and-unityengineobject-destruction-3175</guid>
      <description>&lt;h2&gt;
  
  
  Assumptions
&lt;/h2&gt;

&lt;p&gt;This article assumes &lt;strong&gt;Unity 6.2 or later&lt;/strong&gt;. However, the GC-related explanations are based on the official Unity 6.2 documentation available at the time of writing. Future Unity versions may change implementation details or documentation wording.&lt;/p&gt;

&lt;p&gt;The goal of this article is to avoid mixing up the lifetime-management mechanisms used by C# and Unity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;C# &lt;code&gt;Dispose&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;GC&lt;/li&gt;
&lt;li&gt;finalizers&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UnityEngine.Object&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Addressables&lt;/li&gt;
&lt;li&gt;NativeContainer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Unity 6.2, Incremental GC is enabled by default. On the other hand, &lt;code&gt;System.GC.Collect()&lt;/code&gt; is treated as a full blocking collection, while &lt;code&gt;UnityEngine.Scripting.GarbageCollector.CollectIncremental()&lt;/code&gt; is the Unity-side API for incremental collection.&lt;/p&gt;

&lt;p&gt;In this article, we will separate the following concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the C# managed heap&lt;/li&gt;
&lt;li&gt;external resources closed by &lt;code&gt;IDisposable&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;native Unity engine objects&lt;/li&gt;
&lt;li&gt;GPU resources&lt;/li&gt;
&lt;li&gt;Addressables reference counts&lt;/li&gt;
&lt;li&gt;native memory allocated by NativeContainer types&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you think of all of these as “the GC will clean them up eventually,” Unity will hurt you. Possibly with a chair.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When writing C#, you often hear statements like these:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;C# has GC, so I do not need to free memory manually.&lt;br&gt;&lt;br&gt;
But I still need to call &lt;code&gt;IDisposable&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
Unity &lt;code&gt;Texture&lt;/code&gt; and &lt;code&gt;Material&lt;/code&gt; instances should be closed with &lt;code&gt;Object.Destroy&lt;/code&gt;, not &lt;code&gt;Dispose&lt;/code&gt;, if I created and own those runtime instances.&lt;br&gt;&lt;br&gt;
C# also has &lt;code&gt;~ClassName()&lt;/code&gt;, so can I just clean things up there?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With the right assumptions, each of these statements is true.&lt;/p&gt;

&lt;p&gt;The confusing part is that several different lifetime-management systems are being discussed at once.&lt;/p&gt;

&lt;p&gt;This article looks at &lt;code&gt;Dispose&lt;/code&gt; not as a generic “cleanup method,” but as a way to express ownership. We will also cover why finalizers are not enough, why &lt;code&gt;UnityEngine.Object&lt;/code&gt; uses &lt;code&gt;Destroy&lt;/code&gt;, and why &lt;code&gt;GC.Collect()&lt;/code&gt; is not a magic memory-cleanup button.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Dispose&lt;/code&gt; is not magic for deleting memory.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Dispose&lt;/code&gt; is a contract for explicitly closing a lifetime that the GC cannot understand on its own.&lt;/p&gt;

&lt;p&gt;Unity’s &lt;code&gt;Destroy&lt;/code&gt; is a different mechanism. It tells the Unity engine to destroy an object managed by the engine-side object system.&lt;/p&gt;

&lt;h2&gt;
  
  
  “When references disappear, the object is destroyed” is not quite right
&lt;/h2&gt;

&lt;p&gt;A common explanation of C# GC is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Objects with no references are collected by the GC.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As a rough explanation, that is fine.&lt;/p&gt;

&lt;p&gt;But C# GC is not reference counting. It does not count the number of references to each object and immediately destroy the object when the count reaches zero.&lt;/p&gt;

&lt;p&gt;Roughly speaking, the GC checks whether an object is reachable from &lt;strong&gt;GC Roots&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;GC Roots include references on the stacks of running threads, static fields, CPU register references, GC handles, and runtime-internal references related to finalization. These are representative examples, and the exact details depend on the runtime environment.&lt;/p&gt;

&lt;p&gt;The GC traces the object graph from those roots. Objects that can be reached are considered alive. Objects that cannot be reached are candidates for collection.&lt;/p&gt;

&lt;p&gt;For example, consider this cyclic reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Node&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;Node&lt;/span&gt; &lt;span class="n"&gt;Other&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CreateCycle&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;a&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;Node&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="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Node&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;Other&lt;/span&gt; &lt;span class="p"&gt;=&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Other&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&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;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; refer to each other.&lt;/p&gt;

&lt;p&gt;In a reference-counting system, this kind of cycle can be a problem because the reference count never reaches zero.&lt;/p&gt;

&lt;p&gt;But if neither &lt;code&gt;a&lt;/code&gt; nor &lt;code&gt;b&lt;/code&gt; is reachable from GC Roots, the GC can consider both objects collectible.&lt;/p&gt;

&lt;p&gt;In C#, what matters is not “how many references exist,” but “whether the object is reachable from the live world.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not explain Unity GC exactly like CoreCLR GC
&lt;/h2&gt;

&lt;p&gt;If you search for C# or .NET GC, you will often find explanations about generational GC, Gen0 / Gen1 / Gen2, compaction, and LOH. Those topics are important for CoreCLR, but importing that model directly into Unity can be misleading.&lt;/p&gt;

&lt;p&gt;Unity documentation explains that both Mono and IL2CPP use Boehm GC. In this article, terms like finalizer queue and finalizer thread are used as a general C#/.NET mental model. The exact implementation in Unity depends on the backend.&lt;/p&gt;

&lt;p&gt;In real Unity projects, managed heap behavior is only one part of the picture. You also need to think about Unity engine native memory, assets, GPU resources, Addressables reference counts, and NativeContainer allocations.&lt;/p&gt;

&lt;h2&gt;
  
  
  C# finalizers are not called when references disappear
&lt;/h2&gt;

&lt;p&gt;C# has this syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Sample&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="nf"&gt;Sample&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// finalizer&lt;/span&gt;
        &lt;span class="c1"&gt;// In real code, do not call Unity APIs or access other managed objects here.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;~Sample()&lt;/code&gt; is a &lt;strong&gt;finalizer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It may look like a cleanup method that runs as soon as the object becomes unnecessary, but that is not how it works.&lt;/p&gt;

&lt;p&gt;The rough 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;1. Normal references to the object disappear.
2. Nothing happens immediately.
3. A GC occurs.
4. The GC determines that the object is unreachable.
5. The object becomes a finalization target.
6. The finalizer runs later.
7. The object memory may be reclaimed by a later GC.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More precisely, objects with finalizers are registered for finalization when they are created. When the GC later finds them unreachable, they become eligible for finalizer execution.&lt;/p&gt;

&lt;p&gt;The important point is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A finalizer is not called at the moment references disappear.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Also, even when a GC happens, the finalizer body is not necessarily executed right there inside that GC.&lt;/p&gt;

&lt;p&gt;You cannot control when a finalizer runs. It is also generally unsafe to touch other managed objects or Unity APIs from a finalizer. Having a finalizer can also make GC handling more expensive, and the actual memory for the object may only be reclaimed by a later GC.&lt;/p&gt;

&lt;p&gt;A finalizer is not normal cleanup logic.&lt;/p&gt;

&lt;p&gt;It is a last-resort safety net.&lt;/p&gt;

&lt;p&gt;Files and sockets should be closed with &lt;code&gt;Dispose&lt;/code&gt;. NativeContainers should be closed with &lt;code&gt;Dispose&lt;/code&gt;. Runtime &lt;code&gt;Texture&lt;/code&gt; and &lt;code&gt;Material&lt;/code&gt; instances that you own should be closed with &lt;code&gt;Destroy&lt;/code&gt;. These should not be left to finalizers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Dispose exists
&lt;/h2&gt;

&lt;p&gt;This is where &lt;code&gt;Dispose&lt;/code&gt; comes in.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IDisposable&lt;/code&gt; is a very small interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IDisposable&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;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no magic here.&lt;/p&gt;

&lt;p&gt;Implementing &lt;code&gt;IDisposable&lt;/code&gt; does not make the GC call it immediately. It also does not automatically dispose every field.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Dispose&lt;/code&gt; is just a method.&lt;/p&gt;

&lt;p&gt;It matters because it expresses this contract:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This object owns a resource that the GC cannot fully handle.&lt;br&gt;&lt;br&gt;
Please explicitly close it when you are done.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, here is a simple logger class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FileLogger&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDisposable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;StreamWriter&lt;/span&gt; &lt;span class="n"&gt;_writer&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;FileLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_writer&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;StreamWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Log&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;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&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="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FileLogger&lt;/code&gt; does not directly call an OS file handle API, but it owns a &lt;code&gt;StreamWriter&lt;/code&gt;, and &lt;code&gt;StreamWriter&lt;/code&gt; is &lt;code&gt;IDisposable&lt;/code&gt;. So it is natural for &lt;code&gt;FileLogger&lt;/code&gt; to be &lt;code&gt;IDisposable&lt;/code&gt; too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&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;logger&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;FileLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"log.txt"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;logger&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;"start"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;using&lt;/code&gt;, &lt;code&gt;Dispose&lt;/code&gt; is called when the scope exits. Conceptually, it is close to a &lt;code&gt;try/finally&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In C# 8 or later, you can also write:&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;var&lt;/span&gt; &lt;span class="n"&gt;logger&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;FileLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"log.txt"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;logger&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;"start"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;using var&lt;/code&gt; does not dispose the object immediately after the declaration line. It disposes the object when the variable goes out of scope. If you use it at the beginning of a long method, the lifetime may be longer than you expect. If you want a narrower lifetime, use the traditional &lt;code&gt;using (...) { }&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;The key point is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;using&lt;/code&gt; is not GC.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;using&lt;/code&gt; does not make the GC run earlier. It is syntax for reliably calling &lt;code&gt;Dispose&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;GC checks reachability. &lt;code&gt;Dispose&lt;/code&gt; closes a semantic lifetime. They are not the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dispose means ending ownership, not deleting memory
&lt;/h2&gt;

&lt;p&gt;You typically need &lt;code&gt;Dispose&lt;/code&gt; in the following cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Owning an unmanaged resource directly
&lt;/h3&gt;

&lt;p&gt;For example, you might hold a pointer or handle allocated through a native API.&lt;/p&gt;

&lt;p&gt;The following code is a minimal example for explanation. If &lt;code&gt;Dispose()&lt;/code&gt; is not called, this code leaks native memory. In production code, it is safer to wrap direct &lt;code&gt;IntPtr&lt;/code&gt; ownership with something like &lt;code&gt;SafeHandle&lt;/code&gt; whenever possible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NativeBuffer&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDisposable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IntPtr&lt;/span&gt; &lt;span class="n"&gt;_ptr&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;NativeBuffer&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;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_ptr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AllocHGlobal&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="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_ptr&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;IntPtr&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FreeHGlobal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_ptr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_ptr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;IntPtr&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The memory pointed to by &lt;code&gt;_ptr&lt;/code&gt; is not on the C# managed heap.&lt;/p&gt;

&lt;p&gt;The GC can track the lifetime of the &lt;code&gt;NativeBuffer&lt;/code&gt; object itself. But it does not know how to free the memory behind &lt;code&gt;_ptr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Should it call &lt;code&gt;Marshal.FreeHGlobal&lt;/code&gt;? Should it call some custom &lt;code&gt;Release&lt;/code&gt; function from a native library? The GC cannot infer that.&lt;/p&gt;

&lt;p&gt;That is why &lt;code&gt;Dispose&lt;/code&gt; is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Owning fields that are IDisposable
&lt;/h3&gt;

&lt;p&gt;Even if your class does not directly own native resources, if it owns an &lt;code&gt;IDisposable&lt;/code&gt; object internally, it is usually natural for your class to be &lt;code&gt;IDisposable&lt;/code&gt; too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="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;SaveDataWriter&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDisposable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;BinaryWriter&lt;/span&gt; &lt;span class="n"&gt;_writer&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;SaveDataWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_writer&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;BinaryWriter&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="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;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, &lt;code&gt;BinaryWriter&lt;/code&gt; owns and closes the internal &lt;code&gt;Stream&lt;/code&gt;, so &lt;code&gt;Dispose()&lt;/code&gt; only calls &lt;code&gt;_writer.Dispose()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you create the &lt;code&gt;BinaryWriter&lt;/code&gt; with &lt;code&gt;leaveOpen: true&lt;/code&gt;, then the ownership of the &lt;code&gt;Stream&lt;/code&gt; is separated, and you need to dispose the &lt;code&gt;Stream&lt;/code&gt; elsewhere.&lt;/p&gt;

&lt;p&gt;The point is not “dispose every field you have.”&lt;/p&gt;

&lt;p&gt;The point is deciding &lt;strong&gt;who owns what&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preventing use after disposal
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Dispose&lt;/code&gt; does not delete the C# object itself. The reference can still remain after disposal.&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;writer&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;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"after dispose"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To prevent this kind of misuse, classes often keep a &lt;code&gt;_disposed&lt;/code&gt; flag and throw &lt;code&gt;ObjectDisposedException&lt;/code&gt; when methods are called after disposal.&lt;/p&gt;

&lt;p&gt;This is not about deleting memory. It is about marking the object as no longer usable after its owned resources have been closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  You do not always need the full Dispose Pattern
&lt;/h2&gt;

&lt;p&gt;C# samples often show the full Dispose Pattern with a finalizer, &lt;code&gt;GC.SuppressFinalize(this)&lt;/code&gt;, and &lt;code&gt;protected virtual Dispose(bool disposing)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That pattern is meaningful, but it is not a ritual you need to write every time.&lt;/p&gt;

&lt;p&gt;If you have a &lt;code&gt;sealed&lt;/code&gt; class that only disposes owned &lt;code&gt;IDisposable&lt;/code&gt; fields, a simple &lt;code&gt;Dispose()&lt;/code&gt; is often enough. You do not need a finalizer.&lt;/p&gt;

&lt;p&gt;If you directly own an &lt;code&gt;IntPtr&lt;/code&gt; or other unmanaged resource, you need to think more carefully. Prefer &lt;code&gt;SafeHandle&lt;/code&gt; where possible. If you directly own an unmanaged resource without &lt;code&gt;SafeHandle&lt;/code&gt;, you need some finalizer-like safety net for missed disposal. If the class is intended to be inherited, consider the &lt;code&gt;protected virtual Dispose(bool disposing)&lt;/code&gt; shape.&lt;/p&gt;

&lt;p&gt;The Dispose Pattern is a design choice based on what you own.&lt;/p&gt;

&lt;h2&gt;
  
  
  GC.Collect is not a magic “free memory now” button
&lt;/h2&gt;

&lt;p&gt;When memory usage grows, it is tempting to write:&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;GC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a .NET API, &lt;code&gt;GC.Collect()&lt;/code&gt; forces garbage collection. In Unity 6.2 documentation, &lt;code&gt;System.GC.Collect()&lt;/code&gt; is described as a full blocking collection.&lt;/p&gt;

&lt;p&gt;But the practical point is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Calling it does not guarantee that the amount of memory you expect will immediately go down.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, objects still reachable from GC Roots are not collected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_cache&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;Create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_cache&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="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="n"&gt;GC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Collect&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;Here, &lt;code&gt;_cache&lt;/code&gt; is still referenced from a static field, so the GC considers it alive.&lt;/p&gt;

&lt;p&gt;Objects with finalizers may also not be fully cleaned up by a single GC.&lt;/p&gt;

&lt;p&gt;Most importantly, &lt;code&gt;GC.Collect()&lt;/code&gt; primarily targets the managed heap.&lt;/p&gt;

&lt;p&gt;The following are not fixed by &lt;code&gt;GC.Collect()&lt;/code&gt; alone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deterministically closing files or sockets that were not disposed&lt;/li&gt;
&lt;li&gt;disposing &lt;code&gt;NativeArray&amp;lt;T&amp;gt;&lt;/code&gt; or other NativeContainers&lt;/li&gt;
&lt;li&gt;destroying &lt;code&gt;Texture2D&lt;/code&gt; or &lt;code&gt;Material&lt;/code&gt; instances that you created and own&lt;/li&gt;
&lt;li&gt;Unity engine native memory&lt;/li&gt;
&lt;li&gt;GPU resources&lt;/li&gt;
&lt;li&gt;Addressables reference counts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Depending on implementation, files and sockets may eventually be closed through finalizers or &lt;code&gt;SafeHandle&lt;/code&gt;, but that timing is not deterministic. The owner should close them with &lt;code&gt;using&lt;/code&gt; / &lt;code&gt;Dispose&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Also, Unity’s normal automatic GC may split work across multiple frames through Incremental GC, but that is not the same thing as &lt;code&gt;System.GC.Collect()&lt;/code&gt;. If you want to advance incremental collection explicitly, that is the Unity-side &lt;code&gt;GarbageCollector.CollectIncremental()&lt;/code&gt; API.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;GarbageCollector.GCMode&lt;/code&gt; is set to &lt;code&gt;Disabled&lt;/code&gt;, Unity documentation says that calling &lt;code&gt;System.GC.Collect()&lt;/code&gt; does not start a collection.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;GC.Collect()&lt;/code&gt; is not a fix for lifetime-management bugs. Before calling it, check whether references remain, whether &lt;code&gt;Dispose&lt;/code&gt; was missed, and whether &lt;code&gt;Destroy&lt;/code&gt; was missed.&lt;/p&gt;

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

&lt;p&gt;In Unity, C# code often operates on native Unity engine objects.&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;Texture2D&lt;/span&gt; &lt;span class="n"&gt;texture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Texture2D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Texture2D&lt;/code&gt; looks like a C# object. But it inherits from &lt;code&gt;UnityEngine.Object&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Objects like this are not just ordinary C# managed objects. Roughly speaking, they involve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a C# wrapper&lt;/li&gt;
&lt;li&gt;a native Unity engine object&lt;/li&gt;
&lt;li&gt;sometimes GPU-side resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The C# GC can basically track the C# managed object. Unity engine native memory and GPU resources are outside its direct responsibility.&lt;/p&gt;

&lt;p&gt;So you should not expect the following code to free the native memory behind the texture:&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;Texture2D&lt;/span&gt; &lt;span class="n"&gt;texture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Texture2D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="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="n"&gt;GC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This removes one C# reference and tries to run GC on the managed heap. But the native Unity object and GPU resources behind &lt;code&gt;Texture2D&lt;/code&gt; are not directly released by the GC.&lt;/p&gt;

&lt;p&gt;If you created and own a runtime &lt;code&gt;UnityEngine.Object&lt;/code&gt; instance, such as a &lt;code&gt;new Texture2D&lt;/code&gt;, you usually close that lifetime with &lt;code&gt;Object.Destroy&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;Texture2D&lt;/span&gt; &lt;span class="n"&gt;texture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Texture2D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="c1"&gt;// When you are done with it&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;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the important word here is &lt;strong&gt;ownership&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You should not destroy every &lt;code&gt;UnityEngine.Object&lt;/code&gt; just because it inherits from &lt;code&gt;UnityEngine.Object&lt;/code&gt;. Imported assets, shared assets loaded through &lt;code&gt;Resources.Load&lt;/code&gt;, &lt;code&gt;renderer.sharedMaterial&lt;/code&gt;, and Addressables asset results may be shared by other systems.&lt;/p&gt;

&lt;p&gt;In Unity, destruction is not decided only by type.&lt;/p&gt;

&lt;p&gt;It is decided by who created the object and who owns it.&lt;/p&gt;

&lt;h2&gt;
  
  
  UnityEngine.Object can become “kind of null”
&lt;/h2&gt;

&lt;p&gt;If you have used Unity for a while, you have probably seen behavior 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="n"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texture&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;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="n"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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;Unity’s &lt;code&gt;Object&lt;/code&gt; has special &lt;code&gt;==&lt;/code&gt; behavior that is different from normal C# objects.&lt;/p&gt;

&lt;p&gt;The C# reference can still exist, but the native Unity object behind it may already be destroyed, or scheduled for destruction and treated as invalid by Unity.&lt;/p&gt;

&lt;p&gt;Unity can make that state appear as &lt;code&gt;== null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In plain C#, if a reference remains, it is not null. But with Unity, the C# wrapper points to a native object whose lifetime must also be considered.&lt;/p&gt;

&lt;p&gt;It is often useful to think of &lt;code&gt;UnityEngine.Object&lt;/code&gt; as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a C# wrapper with a handle to a native Unity engine object.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Also note that Unity’s special null behavior depends on the overloaded &lt;code&gt;== null&lt;/code&gt;. &lt;code&gt;ReferenceEquals&lt;/code&gt;, &lt;code&gt;is null&lt;/code&gt;, null-conditional operators, and comparisons under generic constraints may not behave like Unity’s &lt;code&gt;== null&lt;/code&gt; check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why UnityEngine.Object uses Destroy instead of Dispose
&lt;/h2&gt;

&lt;p&gt;A natural question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If Texture owns native resources, why is it not &lt;code&gt;IDisposable&lt;/code&gt;? Why not use &lt;code&gt;Dispose&lt;/code&gt;?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The reason is that Unity’s &lt;code&gt;Object.Destroy&lt;/code&gt; is not just resource cleanup.&lt;/p&gt;

&lt;p&gt;Destroying a &lt;code&gt;GameObject&lt;/code&gt; affects Components, Transform hierarchy, scene management, events, lifecycle callbacks, and more. Destroying a &lt;code&gt;Component&lt;/code&gt; removes it from a GameObject. For &lt;code&gt;MonoBehaviour&lt;/code&gt;, &lt;code&gt;OnDisable&lt;/code&gt; and &lt;code&gt;OnDestroy&lt;/code&gt; may be involved.&lt;/p&gt;

&lt;p&gt;Unity destruction is not just “close the external resource owned by this object.”&lt;/p&gt;

&lt;p&gt;It is an operation that removes an object from Unity’s engine-side object graph.&lt;/p&gt;

&lt;p&gt;Also, &lt;code&gt;Destroy&lt;/code&gt; usually does not immediately destroy the object on the spot. In the normal case, the object is actually destroyed after the current Update loop and before rendering.&lt;/p&gt;

&lt;p&gt;This is useful for maintaining engine consistency during game-loop processing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Destroy&lt;/code&gt; is not C# resource cleanup.&lt;/p&gt;

&lt;p&gt;It is a destruction request to the Unity engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  In Unity, choose the cleanup method by ownership, not just type
&lt;/h2&gt;

&lt;p&gt;It is dangerous to remember cleanup rules only by type.&lt;/p&gt;

&lt;p&gt;“&lt;code&gt;UnityEngine.Object&lt;/code&gt; means &lt;code&gt;Destroy&lt;/code&gt;” is a helpful starting point, but in production code you need to ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I own this object?&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;Target&lt;/th&gt;
&lt;th&gt;Basic policy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plain C# object&lt;/td&gt;
&lt;td&gt;Let GC handle it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C# object implementing &lt;code&gt;IDisposable&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Dispose()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;NativeArray&amp;lt;T&amp;gt;&lt;/code&gt; and other NativeContainers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Dispose()&lt;/code&gt; or &lt;code&gt;Dispose(JobHandle)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime instance created with &lt;code&gt;new Texture2D&lt;/code&gt;, &lt;code&gt;new Material&lt;/code&gt;, or &lt;code&gt;new GameObject&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Owner calls &lt;code&gt;Object.Destroy()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Material implicitly cloned by &lt;code&gt;renderer.material&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Usually tied to the Renderer lifetime. If you need to release it earlier than the Renderer, replace it, or keep ownership yourself, consider &lt;code&gt;Destroy()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;renderer.sharedMaterial&lt;/code&gt;, imported assets, shared asset references&lt;/td&gt;
&lt;td&gt;Do not destroy casually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared asset loaded through &lt;code&gt;Resources.Load&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Do not destroy casually from the user side. Remove references and let it become a candidate for &lt;code&gt;Resources.UnloadUnusedAssets()&lt;/code&gt; when appropriate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asset from Addressables &lt;code&gt;LoadAssetAsync&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Release the corresponding handle or result with &lt;code&gt;Addressables.Release()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instance created by Addressables &lt;code&gt;InstantiateAsync&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;Addressables.ReleaseInstance(gameObject)&lt;/code&gt; or &lt;code&gt;Addressables.ReleaseInstance(handle)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instance created by manually instantiating a Prefab loaded through Addressables&lt;/td&gt;
&lt;td&gt;Treat instance &lt;code&gt;Destroy()&lt;/code&gt; and load-handle &lt;code&gt;Release()&lt;/code&gt; separately. Keep the load handle alive until all instances are destroyed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporary RenderTexture from &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;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;Destroy&lt;/code&gt;, &lt;code&gt;Dispose&lt;/code&gt;, and &lt;code&gt;Release&lt;/code&gt; all close lifetimes, but they close different kinds of lifetimes.&lt;/p&gt;

&lt;p&gt;Mixing them up is how memory bugs get their villain origin story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disposing NativeArray used by Jobs
&lt;/h2&gt;

&lt;p&gt;Unity.Collections NativeContainer types such as &lt;code&gt;NativeArray&amp;lt;T&amp;gt;&lt;/code&gt; implement &lt;code&gt;IDisposable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For synchronous code that does not involve Jobs, the owner disposes it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;array&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;Unity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collections&lt;/span&gt;&lt;span class="p"&gt;.&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;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Unity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collections&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="c1"&gt;// Synchronous usage&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCreated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a Job is using the &lt;code&gt;NativeArray&lt;/code&gt;, do not call &lt;code&gt;Dispose()&lt;/code&gt; while the Job may still be accessing it. Either dispose it after &lt;code&gt;handle.Complete()&lt;/code&gt;, or if the NativeContainer supports it, use &lt;code&gt;Dispose(JobHandle)&lt;/code&gt; to make disposal depend on Job completion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="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;job&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;disposeHandle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&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;disposeHandle&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;p&gt;This example uses &lt;code&gt;Allocator.Persistent&lt;/code&gt; because the point is to show that ownership must be explicitly closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Resources.UnloadUnusedAssets fits in
&lt;/h2&gt;

&lt;p&gt;Unity also has &lt;code&gt;Resources.UnloadUnusedAssets()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is a heavy operation for unloading unused assets and their corresponding Unity-side resources. It can be useful at large loading boundaries or scene transitions.&lt;/p&gt;

&lt;p&gt;But it should not be used as a substitute for normal ownership management.&lt;/p&gt;

&lt;p&gt;First, if unwanted references remain, the asset may not be considered unused. Static fields, event subscriptions, caches, and ScriptableObject references can keep assets alive.&lt;/p&gt;

&lt;p&gt;At the same time, &lt;code&gt;Resources.UnloadUnusedAssets()&lt;/code&gt; does not use exactly the same reachability rules as the C# GC. Unity 6.2 API documentation explains that Unity checks things like the GameObject hierarchy and static variables, but not the script execution stack.&lt;/p&gt;

&lt;p&gt;So an asset referenced only by a temporary local variable is not something you should assume is always protected from &lt;code&gt;UnloadUnusedAssets()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A safer mental model is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dynamically created &lt;code&gt;UnityEngine.Object&lt;/code&gt;: decide the owner and call &lt;code&gt;Destroy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Addressables result: use &lt;code&gt;Release&lt;/code&gt; / &lt;code&gt;ReleaseInstance&lt;/code&gt; depending on how it was acquired&lt;/li&gt;
&lt;li&gt;NativeContainer: &lt;code&gt;Dispose&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Temporary RenderTexture: &lt;code&gt;ReleaseTemporary&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Large loading boundary: consider &lt;code&gt;UnloadUnusedAssets&lt;/code&gt; if needed&lt;/li&gt;
&lt;li&gt;Managed heap issue: inspect references and GC allocations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common misconceptions in real Unity projects
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Setting a variable to null destroys the object
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This only removes one C# reference. If other references remain, the object is not collectible by GC.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;UnityEngine.Object&lt;/code&gt; types, removing the C# reference does not destroy the native Unity object. If you dynamically created and own a Texture or Material, decide the owner and call &lt;code&gt;Destroy&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dispose deletes the C# object
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Dispose&lt;/code&gt; closes owned external resources. It does not delete the C# object immediately.&lt;/p&gt;

&lt;p&gt;The reference still exists after &lt;code&gt;Dispose&lt;/code&gt;, so use a &lt;code&gt;_disposed&lt;/code&gt; flag or throw &lt;code&gt;ObjectDisposedException&lt;/code&gt; if you need to prevent reuse.&lt;/p&gt;

&lt;h3&gt;
  
  
  Destroy means instant total disappearance
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Destroy&lt;/code&gt; tells Unity to destroy the object, but in many cases destruction is not completed immediately on the spot.&lt;/p&gt;

&lt;p&gt;If you keep touching the object in the same frame after calling &lt;code&gt;Destroy&lt;/code&gt;, you may access an object that has already been scheduled for destruction. It is safer to treat a destroyed object as unusable immediately after calling &lt;code&gt;Destroy&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  GC.Collect also cleans up native memory
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;GC.Collect()&lt;/code&gt; primarily works on the managed heap.&lt;/p&gt;

&lt;p&gt;Unity native memory, GPU resources, Addressables reference counts, and NativeContainer allocations have their own lifetime-management mechanisms.&lt;/p&gt;

&lt;p&gt;When memory does not go down in Unity, first ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Who owns this thing, and where is that ownership closed?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The most important thing about &lt;code&gt;Dispose&lt;/code&gt; is not to mix it up with GC.&lt;/p&gt;

&lt;p&gt;GC checks whether managed objects are reachable from GC Roots. C# &lt;code&gt;~ClassName()&lt;/code&gt; is not deterministic destruction; it is a finalizer. &lt;code&gt;Dispose&lt;/code&gt; is a contract for explicitly closing a lifetime the GC cannot understand.&lt;/p&gt;

&lt;p&gt;Unity adds another layer with &lt;code&gt;UnityEngine.Object&lt;/code&gt;. But you should not memorize “all &lt;code&gt;UnityEngine.Object&lt;/code&gt; types must be destroyed.” Instead, ask whether you created and own the runtime instance. Shared assets, Addressables asset results, and &lt;code&gt;renderer.sharedMaterial&lt;/code&gt; should not be casually destroyed by user code.&lt;/p&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plain C# object: let GC handle it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IDisposable&lt;/code&gt;: close it with &lt;code&gt;Dispose&lt;/code&gt; / &lt;code&gt;using&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;NativeContainer: &lt;code&gt;Dispose&lt;/code&gt;, or after Job completion / with &lt;code&gt;Dispose(JobHandle)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Runtime &lt;code&gt;UnityEngine.Object&lt;/code&gt; instance you created and own: &lt;code&gt;Object.Destroy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Addressables: &lt;code&gt;LoadAssetAsync&lt;/code&gt; uses &lt;code&gt;Release&lt;/code&gt;, &lt;code&gt;InstantiateAsync&lt;/code&gt; uses &lt;code&gt;ReleaseInstance&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Temporary RenderTexture: &lt;code&gt;ReleaseTemporary&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Large loading boundary: consider &lt;code&gt;Resources.UnloadUnusedAssets()&lt;/code&gt; when appropriate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;GC.Collect()&lt;/code&gt; does not fix lifetime-management bugs. GC manages the managed heap. Unity native memory, GPU resources, Addressables reference counts, and NativeContainer allocations each have their own lifetime rules.&lt;/p&gt;

&lt;p&gt;Also, &lt;code&gt;UnloadUnusedAssets()&lt;/code&gt; does not use the exact same reachability model as the C# GC. Assets reachable from Unity-side structures such as scene hierarchy or static variables are not treated as unused, but references that only exist on the script execution stack are not necessarily protected.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Dispose&lt;/code&gt; is a contract for closing ownership.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Destroy&lt;/code&gt; is a request to destroy Unity engine objects.&lt;/p&gt;

&lt;p&gt;Keep those two separate, and a lot of Unity memory-management weirdness becomes much easier to reason about.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.2/Documentation/Manual/performance-disabling-garbage-collection.html" rel="noopener noreferrer"&gt;Unity Manual: Configuring garbage collection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.2/Documentation/Manual/performance-incremental-garbage-collection.html" rel="noopener noreferrer"&gt;Unity Manual: Garbage collection modes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.2/Documentation/ScriptReference/Object.Destroy.html" rel="noopener noreferrer"&gt;Unity Scripting API: Object.Destroy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/6000.2/Documentation/ScriptReference/Resources.UnloadUnusedAssets.html" rel="noopener noreferrer"&gt;Unity Scripting API: Resources.UnloadUnusedAssets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.addressables%402.2/manual/MemoryManagement.html" rel="noopener noreferrer"&gt;Unity Addressables 2.2: Memory management overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unity3d.com/Packages/com.unity.addressables%402.2/api/UnityEngine.AddressableAssets.Addressables.ReleaseInstance.html" rel="noopener noreferrer"&gt;Unity Addressables 2.2: Addressables.ReleaseInstance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/dotnet/api/system.gc.collect" rel="noopener noreferrer"&gt;Microsoft Learn: System.GC.Collect&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/finalizers" rel="noopener noreferrer"&gt;Microsoft Learn: Finalizers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/dotnet/standard/design-guidelines/dispose-pattern" rel="noopener noreferrer"&gt;Microsoft Learn: Dispose Pattern&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>gamedev</category>
      <category>memory</category>
    </item>
    <item>
      <title>Token-Saving Techniques for Using AI Efficiently in Unity Projects</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Tue, 30 Jun 2026 08:13:21 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/token-saving-techniques-for-using-ai-efficiently-in-unity-projects-9c1</link>
      <guid>https://dev.to/gamedevtoollab/token-saving-techniques-for-using-ai-efficiently-in-unity-projects-9c1</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Using AI coding agents in a Unity project that is under development or already in production can significantly speed up investigation, implementation, and review.&lt;/p&gt;

&lt;p&gt;At the same time, the larger the project gets, the more likely you are to run into problems like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent reads unrelated directories.&lt;/li&gt;
&lt;li&gt;It tries to inspect generated files such as &lt;code&gt;Library/&lt;/code&gt; or &lt;code&gt;Temp/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It reads huge Scene / Prefab / ScriptableObject YAML files in full.&lt;/li&gt;
&lt;li&gt;It consumes tokens by reading Addressables outputs or large logs.&lt;/li&gt;
&lt;li&gt;You have to repeat the same project explanation in every chat.&lt;/li&gt;
&lt;li&gt;You write too much in &lt;code&gt;AGENTS.md&lt;/code&gt; / &lt;code&gt;CLAUDE.md&lt;/code&gt;, and the agent starts following it less reliably.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part of using AI effectively is not just choosing a powerful model.&lt;/p&gt;

&lt;p&gt;It is &lt;strong&gt;designing what the AI should read, and what it should not read&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article focuses on Unity projects and summarizes practical ways to reduce token usage when using AI agents. At the end, I also include copy-paste friendly &lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt; templates that can be adapted for both Unity and UE5 projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Recommend Writing AGENTS.md / CLAUDE.md in English
&lt;/h2&gt;

&lt;p&gt;The samples in this article write &lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt; in English.&lt;/p&gt;

&lt;p&gt;There are four main reasons.&lt;/p&gt;

&lt;p&gt;First, most codebases are already centered around English. Class names, method names, file names, API names, error messages, and CLI commands are usually English. When instructions are also written in English, they naturally map to the names in the code.&lt;/p&gt;

&lt;p&gt;Second, English rules are easier to reuse across multiple AI agents such as Codex, Claude Code, Gemini CLI, and Cursor. Japanese instructions also work, but English is usually safer for machine-facing team rules.&lt;/p&gt;

&lt;p&gt;Third, English instructions are often easier to keep short. A rule like &lt;code&gt;Do not edit generated files.&lt;/code&gt; is short and clear compared to a polite sentence in Japanese.&lt;/p&gt;

&lt;p&gt;Fourth, it may slightly reduce context size in some cases. This does not mean English always produces dramatically fewer tokens than Japanese. It depends on the model, tokenizer, and how the text is written. Still, for short machine-facing rules, English tends to be compact and makes it easier to remove unnecessary explanation.&lt;/p&gt;

&lt;p&gt;That said, switching to English alone will not magically reduce token usage. The bigger wins come from reducing duplication, clearly excluding directories, and avoiding full reads of large files.&lt;/p&gt;

&lt;p&gt;So you do not need to write everything in English. Human-facing notes, internal terms, and background context can be written in Japanese or whatever language your team uses.&lt;/p&gt;

&lt;p&gt;My recommendation is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write short rules that the AI must follow in English, and write supplemental explanation for humans in your team's preferred language.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;However, if the team cannot confidently review English rules, forcing everything into English is risky. In practice, it is often best to keep AI-facing rules short and English, while adding human-facing background and exceptions in the language the team can maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  First Principle: Instruction Files Are Not Enforcement
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt; are useful, but there is one misunderstanding to clear up first.&lt;/p&gt;

&lt;p&gt;These files are &lt;strong&gt;context&lt;/strong&gt; for AI agents.&lt;br&gt;
They are not hard enforcement settings.&lt;/p&gt;

&lt;p&gt;For example, even if you write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do not inspect Library/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;there is no guarantee that the AI will never read &lt;code&gt;Library/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Of course, writing it is still much better than not writing it. It guides the agent's first moves and reduces unnecessary reads. But it is closer to a request or working policy than a hard rule.&lt;/p&gt;

&lt;p&gt;If you need stronger guarantees, do not rely on instruction files alone. Combine them with layers such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.gitignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.sembleignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Claude Code's &lt;code&gt;claudeMdExcludes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Claude Code hooks&lt;/li&gt;
&lt;li&gt;Codex sandbox / approval / config&lt;/li&gt;
&lt;li&gt;CI / lint / tests&lt;/li&gt;
&lt;li&gt;Keeping generated files out of Git in the first place&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first line of defense for token saving is &lt;code&gt;.gitignore&lt;/code&gt;.&lt;br&gt;
Unity's &lt;code&gt;Library/&lt;/code&gt; and &lt;code&gt;Temp/&lt;/code&gt;, and Unreal's &lt;code&gt;Intermediate/&lt;/code&gt; and &lt;code&gt;Saved/&lt;/code&gt;, should usually not be tracked by Git.&lt;/p&gt;

&lt;p&gt;AI agents may still be able to read untracked files on the local disk, so &lt;code&gt;.gitignore&lt;/code&gt; is not a complete blocker. But it is still very important because it keeps generated files out of search targets, indexes, and diffs.&lt;/p&gt;
&lt;h2&gt;
  
  
  Do Not Let AI Read Everything
&lt;/h2&gt;

&lt;p&gt;The worst thing you can do in a large project is to ask the agent something vague like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Please look at this Unity project and fix the problem.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is like asking a human, "Read every file in this company and improve something."&lt;br&gt;
The AI will try to be diligent and search broadly, which often means reading unrelated files.&lt;/p&gt;

&lt;p&gt;A better request narrows the search scope from the beginning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Investigate only the input validation in the login screen.
First, use Semble to find related files, then report the candidate files and why they matter.
After that, read only the minimum necessary files.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is to specify the &lt;strong&gt;investigation target&lt;/strong&gt; and &lt;strong&gt;search process&lt;/strong&gt; upfront.&lt;/p&gt;

&lt;p&gt;Clarify things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which feature is involved?&lt;/li&gt;
&lt;li&gt;Which directories should be inspected?&lt;/li&gt;
&lt;li&gt;Which directories should not be inspected?&lt;/li&gt;
&lt;li&gt;What search method should be used first?&lt;/li&gt;
&lt;li&gt;Is it allowed to modify files immediately?&lt;/li&gt;
&lt;li&gt;Should the agent report its findings before making changes?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just making these explicit can greatly reduce unnecessary reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Files That Consume Tokens Easily in Unity Projects
&lt;/h2&gt;

&lt;p&gt;Unity projects contain many things that are heavy for AI agents to inspect.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Library/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Contains a huge amount of generated data.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Temp/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Temporary files with low investigation value.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;obj/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;C# build outputs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Logs/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Can be large and repetitive.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;*.csproj&lt;/code&gt; / &lt;code&gt;*.sln&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Usually generated by Unity. Read only when needed; do not edit directly.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scenes / Prefabs&lt;/td&gt;
&lt;td&gt;YAML can become very large.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScriptableObjects&lt;/td&gt;
&lt;td&gt;Can become large depending on data size.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addressables catalogs / bundles&lt;/td&gt;
&lt;td&gt;Heavy as investigation targets.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generated files under StreamingAssets&lt;/td&gt;
&lt;td&gt;Often contains binaries or generated data.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large CSV / JSON / YAML files&lt;/td&gt;
&lt;td&gt;Full reads quickly consume tokens.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the other hand, the places the agent should usually inspect first are often predictable.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Assets/Scripts/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Main gameplay/runtime logic.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Assets/Editor/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Custom Unity Editor extensions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Assets/Plugins/&amp;lt;CompanyName&amp;gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Company-owned runtime/editor code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Packages/manifest.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Package dependencies.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ProjectSettings/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unity project settings. Useful to read, but changes must be handled carefully.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*.asmdef&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Assembly boundaries and dependencies.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Assets/AddressableAssetsData/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Addressables settings.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Depending on Unity and Addressables versions, &lt;code&gt;AddressableAssetsData&lt;/code&gt; can also contain generated files. Avoid full reads of large &lt;code&gt;.bin&lt;/code&gt; files or catalogs; prefer extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with Semble Search
&lt;/h2&gt;

&lt;p&gt;In a large codebase, even using &lt;code&gt;grep&lt;/code&gt; or &lt;code&gt;rg&lt;/code&gt; and then reading several files can consume a lot of tokens.&lt;/p&gt;

&lt;p&gt;Local semantic code search can help. In this article, I use &lt;strong&gt;Semble&lt;/strong&gt; as the example.&lt;/p&gt;

&lt;p&gt;More information about Semble and related case studies is included in the References section at the end. If Semble is new to you, you may want to skim those links first.&lt;/p&gt;

&lt;p&gt;Semble is a code search library for AI agents. It can search code snippets using natural language queries and return relevant snippets instead of entire files. It can be used through MCP, CLI, or instructions in &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its official documentation shows token reduction compared with &lt;code&gt;grep + read&lt;/code&gt;. There is also a Japanese Zenn article that measures token usage reduction when using &lt;code&gt;semble&lt;/code&gt; with Claude Code.&lt;/p&gt;

&lt;p&gt;These numbers depend heavily on the repository, index targets, search queries, and how the AI agent reads results. In this article, I treat Semble as one practical way to implement search-first workflows, not as a guarantee of a fixed reduction rate.&lt;/p&gt;

&lt;p&gt;Also, Semble is not magic. If &lt;code&gt;.gitignore&lt;/code&gt; or &lt;code&gt;.sembleignore&lt;/code&gt; is not configured and generated files are indexed, the search results themselves can become noisy.&lt;/p&gt;

&lt;p&gt;Semble also has limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bad queries return irrelevant results.&lt;/li&gt;
&lt;li&gt;Generated files and huge data files can pollute the index.&lt;/li&gt;
&lt;li&gt;You still need to inspect source files for final confirmation.&lt;/li&gt;
&lt;li&gt;For exact API names or identifiers, &lt;code&gt;rg&lt;/code&gt; may be faster and more accurate.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Search by meaning: Semble
Search exact strings: rg / grep
Inspect huge files: extraction scripts
Final confirmation: read only the necessary original files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A useful &lt;code&gt;AGENTS.md&lt;/code&gt; rule is:&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;## Search policy&lt;/span&gt;

Use Semble first for semantic code search when available and suitable.
Use rg/grep only when exact string matching is needed or Semble results are insufficient.
Do not scan the entire repository without explaining why.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Write the Folder Structure in Advance
&lt;/h2&gt;

&lt;p&gt;Letting the AI infer the project structure every time is wasteful.&lt;/p&gt;

&lt;p&gt;Write a minimal folder map 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;## Project layout&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="sb"&gt;`Assets/Scripts/`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Runtime gameplay code.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Assets/Editor/`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Unity Editor extensions.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Assets/Plugins/CompanyName/`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Company-owned runtime/editor packages.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Assets/Scripts/Generated/`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Generated code. Do not edit directly.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Assets/AddressableAssetsData/`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Addressables settings. Avoid generated binary files.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Packages/`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Unity package manifest and local packages.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`ProjectSettings/`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Unity project settings.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is enough in many cases.&lt;br&gt;
You do not need to document every directory.&lt;/p&gt;

&lt;p&gt;The important part is that the AI can quickly tell where to start and what to avoid.&lt;/p&gt;
&lt;h2&gt;
  
  
  What to Put in AGENTS.md / CLAUDE.md
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt; are not full specifications.&lt;br&gt;
They are work guides for AI agents.&lt;/p&gt;

&lt;p&gt;The useful scope is roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Project overview&lt;/li&gt;
&lt;li&gt;Technologies used&lt;/li&gt;
&lt;li&gt;Important directories&lt;/li&gt;
&lt;li&gt;Directories to avoid&lt;/li&gt;
&lt;li&gt;Search policy&lt;/li&gt;
&lt;li&gt;Generated file policy&lt;/li&gt;
&lt;li&gt;Coding rules&lt;/li&gt;
&lt;li&gt;Work process&lt;/li&gt;
&lt;li&gt;Verification methods&lt;/li&gt;
&lt;li&gt;Things not to change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be careful not to overfill them with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;General explanations of the tech stack&lt;/li&gt;
&lt;li&gt;Long design philosophy&lt;/li&gt;
&lt;li&gt;Outdated specifications&lt;/li&gt;
&lt;li&gt;Rarely used procedures&lt;/li&gt;
&lt;li&gt;Detailed rules for one specific feature&lt;/li&gt;
&lt;li&gt;Large explanations duplicated from README&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Files that the AI reads in every session should stay short.&lt;/p&gt;

&lt;p&gt;Claude Code documentation also explains that long &lt;code&gt;CLAUDE.md&lt;/code&gt; files consume context and may reduce instruction-following reliability. Codex also has a limit on the combined size of &lt;code&gt;AGENTS.md&lt;/code&gt; instructions.&lt;/p&gt;

&lt;p&gt;As a rough guide, keep the root &lt;code&gt;AGENTS.md&lt;/code&gt; around 100 to 200 lines, and move detailed procedures to separate files.&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;## Playbooks&lt;/span&gt;

Read these only when relevant:
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="sb"&gt;`docs/ai/playbooks/addressables.md`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Addressables investigation workflow.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`docs/ai/playbooks/prefab-audit.md`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Scene/Prefab extraction workflow.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`docs/ai/playbooks/release-check.md`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Release verification checklist.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, Claude Code's &lt;code&gt;@path&lt;/code&gt; import expands the imported content into context when it is loaded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Splitting files does not automatically reduce tokens.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The purpose of splitting is to improve maintainability and create a path for the agent to read details only when they are relevant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Differences Between Codex and Claude Code
&lt;/h2&gt;

&lt;p&gt;Codex primarily uses &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;br&gt;
You can combine global instructions in &lt;code&gt;~/.codex/AGENTS.md&lt;/code&gt; with repository-local &lt;code&gt;AGENTS.md&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;Claude Code's standard file name is &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;br&gt;
If you already use &lt;code&gt;AGENTS.md&lt;/code&gt;, you can import it from &lt;code&gt;CLAUDE.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;@AGENTS.md

&lt;span class="gu"&gt;## Claude Code specific rules&lt;/span&gt;

Use plan mode before large refactors.
Do not run Unity Editor unless explicitly requested.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you centralize shared rules in &lt;code&gt;AGENTS.md&lt;/code&gt; and keep only Claude Code-specific rules in &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One important caveat: Codex does not have a standard local file equivalent to &lt;code&gt;CLAUDE.local.md&lt;/code&gt;.&lt;br&gt;
A file named &lt;code&gt;.codex-local.md&lt;/code&gt; is not automatically read.&lt;/p&gt;

&lt;p&gt;Codex instruction discovery basically loads &lt;strong&gt;at most one instruction file per directory&lt;/strong&gt;. The search order is &lt;code&gt;AGENTS.override.md&lt;/code&gt;, then &lt;code&gt;AGENTS.md&lt;/code&gt;, then names registered in &lt;code&gt;project_doc_fallback_filenames&lt;/code&gt;. If &lt;code&gt;AGENTS.md&lt;/code&gt; exists in the same directory, adding &lt;code&gt;.codex-local.md&lt;/code&gt; as a fallback name does not make it behave like &lt;code&gt;CLAUDE.local.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Also, as its name implies, &lt;code&gt;AGENTS.override.md&lt;/code&gt; is an override. If you place it at the repository root, it may replace the team-shared &lt;code&gt;AGENTS.md&lt;/code&gt; at the same level rather than supplementing it.&lt;/p&gt;

&lt;p&gt;If you want personal rules for Codex, consider this order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write cross-repository personal rules in &lt;code&gt;~/.codex/AGENTS.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;CODEX_HOME&lt;/code&gt; if you want to separate working profiles.&lt;/li&gt;
&lt;li&gt;Use repository-local &lt;code&gt;AGENTS.override.md&lt;/code&gt; only when the team has agreed on its meaning.&lt;/li&gt;
&lt;li&gt;For temporary personal notes, manually ask the agent to read them at the start of the session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is safer to separate team-shared rules from personal notes. In Claude Code, &lt;code&gt;CLAUDE.local.md&lt;/code&gt; is suitable for that purpose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# personal AI agent notes
CLAUDE.local.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use a custom name with Codex, treat it as a fallback candidate for directories without &lt;code&gt;AGENTS.md&lt;/code&gt;, not as an append-only supplement to an existing &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extract Large Data Instead of Reading It Directly
&lt;/h2&gt;

&lt;p&gt;If you let the AI read full Scenes, Prefabs, ScriptableObjects, logs, CSV, JSON, or YAML files, tokens disappear fast.&lt;/p&gt;

&lt;p&gt;In these cases, it is often more efficient to make the AI &lt;strong&gt;write an extraction script&lt;/strong&gt; instead of reading the file directly.&lt;/p&gt;

&lt;p&gt;For example, when investigating Prefabs, do not read the entire YAML. Use an Editor script to output only the needed information.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Extracted information&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prefab / Scene&lt;/td&gt;
&lt;td&gt;GameObject hierarchy, component list, missing references.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScriptableObject&lt;/td&gt;
&lt;td&gt;Type name, key fields, referenced GUIDs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addressables&lt;/td&gt;
&lt;td&gt;Address, label, group, path.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;Error lines, N surrounding lines, duplicate counts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSV / JSON&lt;/td&gt;
&lt;td&gt;Row count, keys, problematic entries, a few samples.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A useful &lt;code&gt;AGENTS.md&lt;/code&gt; rule is:&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;## Large file policy&lt;/span&gt;

Do not read large YAML, JSON, CSV, log, scene, prefab, or ScriptableObject files directly.

For large files:
&lt;span class="p"&gt;1.&lt;/span&gt; Write a small read-only script to extract only relevant fields.
&lt;span class="p"&gt;2.&lt;/span&gt; Show the script before running it when it may touch Unity/Unreal assets.
&lt;span class="p"&gt;3.&lt;/span&gt; Do not call SaveAssets, ForceReserializeAssets, asset delete/move APIs, or import-triggering operations unless explicitly requested.
&lt;span class="p"&gt;4.&lt;/span&gt; Write extraction output to a dedicated gitignored report directory such as &lt;span class="sb"&gt;`.ai-tmp/ai-extract/`&lt;/span&gt;.
&lt;span class="p"&gt;5.&lt;/span&gt; Show the extraction summary.
&lt;span class="p"&gt;6.&lt;/span&gt; Read the original file only if the summary is insufficient.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point is to avoid saying "read everything" from the start.&lt;/p&gt;

&lt;p&gt;Make the AI narrow the data first. For logs, that means error types and a few surrounding lines. For Prefabs, that means GameObject paths, component names, and missing references.&lt;/p&gt;

&lt;p&gt;For extraction output, avoid Unity's &lt;code&gt;Temp/&lt;/code&gt;. Use a project-controlled gitignored directory such as &lt;code&gt;.ai-tmp/ai-extract/&lt;/code&gt;. &lt;code&gt;Temp/&lt;/code&gt; is managed by the Unity Editor and is not a stable workspace for AI-generated reports.&lt;/p&gt;

&lt;p&gt;When running Unity Editor scripts, assume &lt;strong&gt;read-only&lt;/strong&gt; by default. Investigation scripts should generally not call &lt;code&gt;AssetDatabase.SaveAssets()&lt;/code&gt;, &lt;code&gt;ForceReserializeAssets()&lt;/code&gt;, asset delete/move APIs, or anything that triggers reimport. It is also useful to confirm that the Git working tree is clean before running such scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do Not Edit Generated Code Directly
&lt;/h2&gt;

&lt;p&gt;API clients, master data definitions, localization definitions, Protocol Buffers, and C# outputs from internal tools are overwritten the next time they are generated.&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;## Generated files&lt;/span&gt;

Do not edit generated files directly.

Examples:
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Assets/Scripts/Generated/`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Assets/Plugins/CompanyName/Generated/`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`*.g.cs`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`*.generated.cs`&lt;/span&gt;

If a generated file has a bug, fix the generator or template instead.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just stating "fix the source generator instead" prevents many accidents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask for Findings Before Changes
&lt;/h2&gt;

&lt;p&gt;If you let the AI immediately modify files, it may touch unrelated code.&lt;/p&gt;

&lt;p&gt;For larger tasks, make the process explicit:&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;## Work process&lt;/span&gt;

For non-trivial tasks:
&lt;span class="p"&gt;
1.&lt;/span&gt; Search relevant files first.
&lt;span class="p"&gt;2.&lt;/span&gt; Report the files you plan to inspect.
&lt;span class="p"&gt;3.&lt;/span&gt; Summarize the current behavior.
&lt;span class="p"&gt;4.&lt;/span&gt; Propose the smallest safe change.
&lt;span class="p"&gt;5.&lt;/span&gt; Modify only the necessary files.
&lt;span class="p"&gt;6.&lt;/span&gt; Run the relevant verification command if available.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows a human to check the search scope.&lt;/p&gt;

&lt;p&gt;In Unity, one feature can involve Runtime code, Editor tools, Addressables, ScriptableObjects, and Prefabs at the same time.&lt;br&gt;
Asking the agent to report where it will look first reduces both unnecessary reading and incorrect edits.&lt;/p&gt;
&lt;h2&gt;
  
  
  Keep Changes Small
&lt;/h2&gt;

&lt;p&gt;AI agents sometimes try to clean up extra things while solving a task.&lt;br&gt;
That can be a problem in production projects.&lt;/p&gt;

&lt;p&gt;Unrequested refactoring increases review cost.&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;## Change policy&lt;/span&gt;

Keep changes minimal.
Do not perform broad refactors unless explicitly requested.
Do not change public APIs unless required.
Do not reformat unrelated files.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Making "minimal changes" a rule keeps reviews manageable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write Verification Commands
&lt;/h2&gt;

&lt;p&gt;If you ask the AI to "verify it" without defining how, the result will be vague.&lt;/p&gt;

&lt;p&gt;Write project-specific verification methods.&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;## Verification&lt;/span&gt;

Prefer the smallest relevant verification.

Examples:
&lt;span class="p"&gt;-&lt;/span&gt; Run EditMode tests from Unity Test Runner.
&lt;span class="p"&gt;-&lt;/span&gt; Run PlayMode tests only when the change affects runtime behavior.
&lt;span class="p"&gt;-&lt;/span&gt; Run Unity batchmode tests when available.
&lt;span class="p"&gt;-&lt;/span&gt; Run .NET tool tests with &lt;span class="sb"&gt;`dotnet test`&lt;/span&gt; only for standalone tool projects.

If verification cannot be run, explain what should be checked manually.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your project runs Unity Test Runner from the command line, include the actual command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Unity batchmode example:

&lt;span class="sb"&gt;`Unity.exe -runTests -batchmode -projectPath . -testPlatform EditMode -testResults TestResults.xml`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unity Test Framework examples for &lt;code&gt;-runTests&lt;/code&gt; do not include &lt;code&gt;-quit&lt;/code&gt;. &lt;code&gt;-quit&lt;/code&gt; is often used in normal batchmode build commands, but it is safer not to mix it into test execution examples.&lt;/p&gt;

&lt;p&gt;Unity CLI execution differs by environment. Use your team's actual command rather than treating article snippets as universal. &lt;code&gt;Unity.exe&lt;/code&gt; is a Windows example; real commands depend on Unity Hub installation paths, OS, CI image, licensing, and &lt;code&gt;-logFile&lt;/code&gt; handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Personal Notes Out of Git
&lt;/h2&gt;

&lt;p&gt;Separate team-shared &lt;code&gt;AGENTS.md&lt;/code&gt; / &lt;code&gt;CLAUDE.md&lt;/code&gt; files from machine-local personal notes.&lt;/p&gt;

&lt;p&gt;In Claude Code, you can use &lt;code&gt;CLAUDE.local.md&lt;/code&gt; and add it to &lt;code&gt;.gitignore&lt;/code&gt;. It is a good place for local paths, personal commands, and temporary investigation notes.&lt;/p&gt;

&lt;p&gt;Codex does not have a standard local supplement equivalent to &lt;code&gt;CLAUDE.local.md&lt;/code&gt;. For personal Codex rules, prefer &lt;code&gt;~/.codex/AGENTS.md&lt;/code&gt;. Use &lt;code&gt;AGENTS.override.md&lt;/code&gt; only when the team agrees on its meaning, because it may replace the same-level &lt;code&gt;AGENTS.md&lt;/code&gt; rather than supplement it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLAUDE.local.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Unreal Engine Notes
&lt;/h2&gt;

&lt;p&gt;The same idea applies to UE5.&lt;br&gt;
Separate what the AI should inspect from what it should avoid.&lt;/p&gt;

&lt;p&gt;Prefer reading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source/
Config/
Plugins/*/Source/
*.Build.cs
*.Target.cs
*.uproject
*.uplugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Intermediate/
Saved/
DerivedDataCache/
Binaries/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.uasset&lt;/code&gt; and &lt;code&gt;.umap&lt;/code&gt; files under &lt;code&gt;Content/&lt;/code&gt; are not useful as raw text.&lt;br&gt;
For Blueprint or Map investigation, extract the necessary information through Asset Registry, Editor Utility, Python scripts, or C++ helper commands before giving it to the AI.&lt;/p&gt;

&lt;p&gt;UE contains even more binary assets than Unity, so it is especially important to think in terms of &lt;strong&gt;extracting information through the editor&lt;/strong&gt; rather than &lt;strong&gt;asking the AI to read files&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Chat Prompt Still Matters
&lt;/h2&gt;

&lt;p&gt;Even with a good &lt;code&gt;AGENTS.md&lt;/code&gt;, vague chat instructions reduce the effect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Investigate the NullReferenceException that appears during Unity startup.
First, use Semble to find likely initialization code.
Do not read the full log. Check only the error and 5 surrounding lines.
Before changing anything, report possible causes and the files you plan to modify.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good requests include the target feature, first search method, things not to read, and a request to report before modifying.&lt;br&gt;
The important part is not prompt elegance. It is &lt;strong&gt;scoping the investigation&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimum Checklist
&lt;/h2&gt;

&lt;p&gt;Before publishing or introducing this workflow to a project, check at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.gitignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;directories to read and avoid&lt;/li&gt;
&lt;li&gt;generated file policy&lt;/li&gt;
&lt;li&gt;large-file extraction policy&lt;/li&gt;
&lt;li&gt;verification methods&lt;/li&gt;
&lt;li&gt;separation of personal notes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even this alone can greatly reduce wasteful AI reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Reducing token usage in AI agents is not just about choosing the right model.&lt;/p&gt;

&lt;p&gt;The important part is designing what information the AI reads.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep generated files out of version control with &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Write search policies in &lt;code&gt;AGENTS.md&lt;/code&gt; / &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Understand that instruction files are context, not enforcement.&lt;/li&gt;
&lt;li&gt;Use search tools such as Semble first.&lt;/li&gt;
&lt;li&gt;Extract summaries from huge files instead of reading them directly.&lt;/li&gt;
&lt;li&gt;Ask for findings before changes.&lt;/li&gt;
&lt;li&gt;Keep changes small.&lt;/li&gt;
&lt;li&gt;Define verification methods.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unity projects contain many files that are expensive for AI agents to read.&lt;br&gt;
That is exactly why &lt;strong&gt;not letting the AI read everything&lt;/strong&gt; is so effective.&lt;/p&gt;

&lt;p&gt;Finally, here are general-purpose templates for Unity and UE5.&lt;/p&gt;

&lt;p&gt;The following section intentionally repeats some of the article in a copy-paste friendly form. It is kept as an independent template so it can be pasted into real projects.&lt;/p&gt;

&lt;p&gt;Do not use everything blindly. Delete what you do not need. Unity-specific and Unreal-specific sections are placed at the bottom of &lt;code&gt;AGENTS.md&lt;/code&gt;; for a Unity-only project, delete the Unreal section, and for an Unreal-only project, delete the Unity section.&lt;/p&gt;




&lt;h2&gt;
  
  
  Copy-Paste AGENTS.md
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# AGENTS.md&lt;/span&gt;

&lt;span class="gu"&gt;## Language&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Prefer short English for machine-facing rules.
&lt;span class="p"&gt;-&lt;/span&gt; Use a language the team can review and maintain.
&lt;span class="p"&gt;-&lt;/span&gt; Reply to users in the language they use.

&lt;span class="gu"&gt;## Project overview&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; This is a game development project using Unity and/or Unreal Engine.
&lt;span class="p"&gt;-&lt;/span&gt; Prioritize small, safe, reviewable changes.
&lt;span class="p"&gt;-&lt;/span&gt; Do not perform broad refactors unless explicitly requested.

&lt;span class="gu"&gt;## Core rules&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Do not read the entire repository by default.
&lt;span class="p"&gt;-&lt;/span&gt; Search first, inspect only relevant files, then propose the smallest safe change.
&lt;span class="p"&gt;-&lt;/span&gt; Instruction files are guidance, not enforcement.
&lt;span class="p"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`.gitignore`&lt;/span&gt;, tool settings, hooks, tests, and CI for hard guarantees.

&lt;span class="gu"&gt;## Search policy&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Use Semble first for semantic code search when available and suitable.
&lt;span class="p"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`rg`&lt;/span&gt; / &lt;span class="sb"&gt;`grep`&lt;/span&gt; for exact symbols, APIs, errors, and paths.
&lt;span class="p"&gt;-&lt;/span&gt; Do not scan the entire repository without explaining why.

&lt;span class="gu"&gt;## Workflow for non-trivial changes&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Search for relevant files.
&lt;span class="p"&gt;2.&lt;/span&gt; Report candidate files and why they matter.
&lt;span class="p"&gt;3.&lt;/span&gt; Inspect only necessary files.
&lt;span class="p"&gt;4.&lt;/span&gt; Summarize current behavior.
&lt;span class="p"&gt;5.&lt;/span&gt; Propose the smallest safe change.

&lt;span class="gu"&gt;## Avoid by default&lt;/span&gt;
Do not inspect these unless explicitly requested:
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`.git/`&lt;/span&gt;, &lt;span class="sb"&gt;`.vs/`&lt;/span&gt;, &lt;span class="sb"&gt;`.idea/`&lt;/span&gt;, &lt;span class="sb"&gt;`.gradle/`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`node_modules/`&lt;/span&gt;, &lt;span class="sb"&gt;`dist/`&lt;/span&gt;, &lt;span class="sb"&gt;`build/`&lt;/span&gt;, &lt;span class="sb"&gt;`Build/`&lt;/span&gt;, &lt;span class="sb"&gt;`Builds/`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`logs/`&lt;/span&gt;, &lt;span class="sb"&gt;`MemoryCaptures/`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`.ai-tmp/`&lt;/span&gt;, except report files created for the current task

&lt;span class="gu"&gt;## Large files&lt;/span&gt;
Do not read large YAML, JSON, CSV, logs, scenes, prefabs, ScriptableObjects, &lt;span class="sb"&gt;`.uasset`&lt;/span&gt;, or &lt;span class="sb"&gt;`.umap`&lt;/span&gt; files directly.
&lt;span class="p"&gt;-&lt;/span&gt; Create a small read-only extraction script or command first.
&lt;span class="p"&gt;-&lt;/span&gt; Show the script before running it when it may touch assets.
&lt;span class="p"&gt;-&lt;/span&gt; Do not save, reserialize, delete, move, reimport, or modify assets unless explicitly requested.
&lt;span class="p"&gt;-&lt;/span&gt; Write reports to &lt;span class="sb"&gt;`.ai-tmp/ai-extract/`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Read only the generated summary first.
&lt;span class="p"&gt;-&lt;/span&gt; Read raw files only if the summary is insufficient.

&lt;span class="gu"&gt;## Generated files&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Do not edit generated files directly.
&lt;span class="p"&gt;-&lt;/span&gt; Examples: &lt;span class="sb"&gt;`**/Generated/**`&lt;/span&gt;, &lt;span class="sb"&gt;`*.g.cs`&lt;/span&gt;, &lt;span class="sb"&gt;`*.generated.cs`&lt;/span&gt;, generated API clients, data loaders, localization files, protobuf/message files.
&lt;span class="p"&gt;-&lt;/span&gt; If generated output has a bug, fix the generator, template, or source data instead.

&lt;span class="gu"&gt;## Coding policy&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Follow existing style.
&lt;span class="p"&gt;-&lt;/span&gt; Keep changes minimal.
&lt;span class="p"&gt;-&lt;/span&gt; Do not reformat unrelated files.
&lt;span class="p"&gt;-&lt;/span&gt; Do not change public APIs unless required.
&lt;span class="p"&gt;-&lt;/span&gt; Do not introduce new packages without approval.
&lt;span class="p"&gt;-&lt;/span&gt; Prefer readable code over clever code.

&lt;span class="gu"&gt;## Verification&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Run the smallest relevant verification when possible.
&lt;span class="p"&gt;-&lt;/span&gt; Use the project's actual commands rather than generic examples.
&lt;span class="p"&gt;-&lt;/span&gt; If verification cannot be run, explain why and what should be checked manually.

&lt;span class="gu"&gt;## Reporting&lt;/span&gt;
When making changes, report:
&lt;span class="p"&gt;-&lt;/span&gt; files inspected
&lt;span class="p"&gt;-&lt;/span&gt; files changed
&lt;span class="p"&gt;-&lt;/span&gt; reason for each change
&lt;span class="p"&gt;-&lt;/span&gt; verification performed
&lt;span class="p"&gt;-&lt;/span&gt; verification not performed
&lt;span class="p"&gt;-&lt;/span&gt; remaining risks

&lt;span class="gu"&gt;## Platform-specific sections&lt;/span&gt;
Unity and Unreal Engine rules are intentionally placed at the end.
Delete the section that does not apply to your project.

&lt;span class="gu"&gt;## Unity policy&lt;/span&gt;
Prefer reading:
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Assets/Scripts/`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Assets/Editor/`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Assets/Plugins/&amp;lt;CompanyName&amp;gt;/`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Treat third-party plugins and purchased assets as read-only unless explicitly requested.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Packages/manifest.json`&lt;/span&gt;, &lt;span class="sb"&gt;`Packages/packages-lock.json`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`ProjectSettings/`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Read when relevant, but change only with explicit intent and explain the diff.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Assets/**/*.asmdef`&lt;/span&gt;

Avoid unless explicitly requested:
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Library/`&lt;/span&gt;, &lt;span class="sb"&gt;`Temp/`&lt;/span&gt;, &lt;span class="sb"&gt;`temp/`&lt;/span&gt;, &lt;span class="sb"&gt;`obj/`&lt;/span&gt;, &lt;span class="sb"&gt;`Obj/`&lt;/span&gt;, &lt;span class="sb"&gt;`Logs/`&lt;/span&gt;, &lt;span class="sb"&gt;`UserSettings/`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`*.csproj`&lt;/span&gt;, &lt;span class="sb"&gt;`*.sln`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Usually generated by Unity. Read only when needed; do not edit directly.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Assets/StreamingAssets/aa/`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; large generated Addressables files, bundles, catalogs, and binary outputs

For serialized assets, extract path, GUID, GameObject path, component type, serialized fields, missing references, and Addressables address/group/labels.
Verification examples: EditMode tests, PlayMode tests, Unity batchmode tests without adding &lt;span class="sb"&gt;`-quit`&lt;/span&gt; to &lt;span class="sb"&gt;`-runTests`&lt;/span&gt;, project-specific validation scripts.

&lt;span class="gu"&gt;## Unreal Engine policy&lt;/span&gt;
Prefer reading:
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Source/`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Config/`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Plugins/*/Source/`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`*.Build.cs`&lt;/span&gt;, &lt;span class="sb"&gt;`*.Target.cs`&lt;/span&gt;, &lt;span class="sb"&gt;`*.uproject`&lt;/span&gt;, &lt;span class="sb"&gt;`*.uplugin`&lt;/span&gt;

Avoid unless explicitly requested:
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Binaries/`&lt;/span&gt;, &lt;span class="sb"&gt;`Intermediate/`&lt;/span&gt;, &lt;span class="sb"&gt;`Saved/`&lt;/span&gt;, &lt;span class="sb"&gt;`DerivedDataCache/`&lt;/span&gt;

Do not treat &lt;span class="sb"&gt;`.uasset`&lt;/span&gt; or &lt;span class="sb"&gt;`.umap`&lt;/span&gt; files as readable source.
For Blueprint, Map, Asset, or Content analysis, extract metadata through Unreal Editor, Asset Registry, Editor Utility, Python, or C++ helper scripts.
Verification examples: build the affected target, run relevant automation tests, run project-specific validation scripts.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Copy-Paste CLAUDE.md
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;@AGENTS.md

&lt;span class="gh"&gt;# CLAUDE.md&lt;/span&gt;

&lt;span class="gu"&gt;## Claude Code specific rules&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Use plan mode before large refactors or multi-file changes.
&lt;span class="p"&gt;-&lt;/span&gt; Do not run Unity Editor or Unreal Editor unless explicitly requested.
&lt;span class="p"&gt;-&lt;/span&gt; If an editor command is necessary, explain the command and expected impact first.
&lt;span class="p"&gt;-&lt;/span&gt; Prefer Semble or narrow searches over broad Read/Grep/Glob operations.
&lt;span class="p"&gt;-&lt;/span&gt; For large files or asset investigation, use read-only extraction and read the summary first.

&lt;span class="gu"&gt;## Context management&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Keep this file short.
&lt;span class="p"&gt;-&lt;/span&gt; Put long task-specific procedures in separate files and read them only when relevant.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`@path`&lt;/span&gt; imports help maintainability, but imported content is still loaded into context.
&lt;span class="p"&gt;-&lt;/span&gt; Do not import large files unless they are needed in every session.

&lt;span class="gu"&gt;## Personal notes&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`CLAUDE.local.md`&lt;/span&gt; for personal, machine-specific notes.
&lt;span class="p"&gt;-&lt;/span&gt; Do not commit &lt;span class="sb"&gt;`CLAUDE.local.md`&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;OpenAI Codex: Custom instructions with AGENTS.md
&lt;a href="https://developers.openai.com/codex/guides/agents-md" rel="noopener noreferrer"&gt;https://developers.openai.com/codex/guides/agents-md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Claude Code: Memory / CLAUDE.md
&lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;https://code.claude.com/docs/en/memory&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Claude Code: Hooks reference
&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;https://code.claude.com/docs/en/hooks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unity Docs: Recommended ignore configuration for Unity projects
&lt;a href="https://docs.unity.com/unity-version-control/ignore-files" rel="noopener noreferrer"&gt;https://docs.unity.com/unity-version-control/ignore-files&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unity Docs: Running tests from the command line
&lt;a href="https://docs.unity.cn/Packages/com.unity.test-framework%401.4/manual/reference-command-line.html" rel="noopener noreferrer"&gt;https://docs.unity.cn/Packages/com.unity.test-framework%401.4/manual/reference-command-line.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unreal Engine Documentation: Directory Structure
&lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-engine-directory-structure" rel="noopener noreferrer"&gt;https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-engine-directory-structure&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Semble official documentation
&lt;a href="https://minish.ai/packages/semble/introduction/" rel="noopener noreferrer"&gt;https://minish.ai/packages/semble/introduction/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Semble GitHub
&lt;a href="https://github.com/MinishLab/semble" rel="noopener noreferrer"&gt;https://github.com/MinishLab/semble&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zenn: Claude Code のトークン削減を実測した — semble 93%・cacheRead 1800倍の内訳
&lt;a href="https://zenn.dev/pepabo/articles/claude-code-token-reduction-measured" rel="noopener noreferrer"&gt;https://zenn.dev/pepabo/articles/claude-code-token-reduction-measured&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>ai</category>
      <category>claude</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Creating a Small Helper Class to Safely Handle CancellationTokenSource Cancel and Dispose</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Fri, 26 Jun 2026 02:21:58 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/creating-a-small-helper-class-to-safely-handle-cancellationtokensource-cancel-and-dispose-11ca</link>
      <guid>https://dev.to/gamedevtoollab/creating-a-small-helper-class-to-safely-handle-cancellationtokensource-cancel-and-dispose-11ca</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When writing async code in Unity, you often end up working with &lt;code&gt;CancellationToken&lt;/code&gt; for loading, networking, Addressables, screen transitions, timeouts, and so on.&lt;/p&gt;

&lt;p&gt;The object you usually create for that is &lt;code&gt;CancellationTokenSource&lt;/code&gt;, but it is surprisingly easy to handle it incorrectly.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_cts&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;CancelAndDispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_cts&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_cts&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, this looks fine. However, the disposed &lt;code&gt;CancellationTokenSource&lt;/code&gt; remains in the field.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;CancelAndDispose()&lt;/code&gt; is called again, the second &lt;code&gt;Cancel()&lt;/code&gt; may throw an &lt;code&gt;ObjectDisposedException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Also, when using &lt;code&gt;CancelAfter()&lt;/code&gt; or &lt;code&gt;CreateLinkedTokenSource()&lt;/code&gt;, disposing the CTS after use becomes even more important.&lt;/p&gt;

&lt;p&gt;This article explains the difference between &lt;code&gt;Cancel()&lt;/code&gt; and &lt;code&gt;Dispose()&lt;/code&gt;, then introduces a small helper class that safely handles &lt;code&gt;CancelAfter()&lt;/code&gt; and &lt;code&gt;CreateLinkedTokenSource()&lt;/code&gt; as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cancel and Dispose Are Different Things
&lt;/h2&gt;

&lt;p&gt;First, &lt;code&gt;Cancel()&lt;/code&gt; and &lt;code&gt;Dispose()&lt;/code&gt; do different jobs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Cancel()&lt;/code&gt; sends a cancellation request to code that has received a &lt;code&gt;CancellationToken&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cts&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;CancellationTokenSource&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;task&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is only a cancellation request.&lt;/p&gt;

&lt;p&gt;Calling &lt;code&gt;Cancel()&lt;/code&gt; does not forcibly stop a running operation. The callee must either check the &lt;code&gt;CancellationToken&lt;/code&gt; or pass it to an API that supports cancellation.&lt;/p&gt;

&lt;p&gt;On the other hand, &lt;code&gt;Dispose()&lt;/code&gt; is not a cancellation request.&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;cts&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Dispose()&lt;/code&gt; releases resources held internally by &lt;code&gt;CancellationTokenSource&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cancel()
    Sends a cancellation request

Dispose()
    Releases resources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling &lt;code&gt;Cancel()&lt;/code&gt; does not automatically call &lt;code&gt;Dispose()&lt;/code&gt;. Depending on the situation, you may need both.&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;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;cts&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, that does not mean you should blindly write those two lines everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disposed Does Not Mean Null
&lt;/h2&gt;

&lt;p&gt;Let's look at this code again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_cts&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;CancelAndDispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_cts&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_cts&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Dispose()&lt;/code&gt; does not set &lt;code&gt;_cts&lt;/code&gt; to &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That means the following call order can be dangerous:&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="nf"&gt;CancelAndDispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;CancelAndDispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even after &lt;code&gt;_cts.Dispose()&lt;/code&gt; is called the first time, &lt;code&gt;_cts&lt;/code&gt; is still not &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the second call still executes &lt;code&gt;_cts?.Cancel()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CancellationTokenSource.Cancel()&lt;/code&gt; may throw &lt;code&gt;ObjectDisposedException&lt;/code&gt; if the target CTS has already been disposed.&lt;/p&gt;

&lt;p&gt;In Unity, cleanup code can be called through multiple paths.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;CancelAndDispose&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;CancelAndDispose&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;You could catch and ignore &lt;code&gt;ObjectDisposedException&lt;/code&gt;, but that is not the real fix.&lt;/p&gt;

&lt;p&gt;The real problem is that a disposed CTS remains in the field.&lt;/p&gt;

&lt;p&gt;Instead of hiding the exception, it is safer to structure the code so that a disposed CTS is never touched again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dispose Is Important with CancelAfter and CreateLinkedTokenSource
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CancelAfter()&lt;/code&gt; schedules a cancellation request after a specified delay.&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;var&lt;/span&gt; &lt;span class="n"&gt;cts&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;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CancelAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is commonly used for timeouts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CancelAfter()&lt;/code&gt; is convenient, but the CTS needs internal machinery to cancel itself later. Because of that, it is important to dispose the CTS when you are done with it.&lt;/p&gt;

&lt;p&gt;The same applies to &lt;code&gt;CreateLinkedTokenSource()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, you may want to combine multiple cancellation conditions, such as "the MonoBehaviour was destroyed" and "the operation timed out".&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;var&lt;/span&gt; &lt;span class="n"&gt;timeoutCts&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;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;timeoutCts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CancelAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&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;linkedCts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateLinkedTokenSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;destroyCancellationToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeoutCts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;linkedCts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, both &lt;code&gt;timeoutCts&lt;/code&gt; and &lt;code&gt;linkedCts&lt;/code&gt; should be disposed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CreateLinkedTokenSource()&lt;/code&gt; creates a new &lt;code&gt;CancellationTokenSource&lt;/code&gt; that becomes canceled when any of the provided tokens is canceled.&lt;/p&gt;

&lt;p&gt;So this newly created CTS is also something you should dispose after use.&lt;/p&gt;

&lt;p&gt;If you leave a CTS created with &lt;code&gt;CancelAfter()&lt;/code&gt; or &lt;code&gt;CreateLinkedTokenSource()&lt;/code&gt; undisposed, the release of internal resources and registrations may be delayed.&lt;/p&gt;

&lt;p&gt;Instead of saying this always causes an immediate memory leak, it is more accurate to say that resources owned by the finished CTS may remain alive longer than necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing CTS Fields Directly Gets Messy
&lt;/h2&gt;

&lt;p&gt;If everything is contained inside a single method, &lt;code&gt;using&lt;/code&gt; is often enough.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cts&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;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CancelAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, in game code, you often want to keep a CTS in a field.&lt;/p&gt;

&lt;p&gt;For example, when restarting a load operation, you may want to cancel the previous load first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_loadCts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Reload&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_loadCts&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_loadCts&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;_loadCts&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;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_loadCts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OperationCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Cancellation is treated as a normal flow.&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 code mixes together cancellation of the previous CTS, disposal, creation of a new CTS, timeout setup, linked token creation, and protection against double cleanup.&lt;/p&gt;

&lt;p&gt;Writing that every time is annoying.&lt;/p&gt;

&lt;p&gt;And annoying code tends to get copied incorrectly sooner or later.&lt;/p&gt;

&lt;p&gt;So instead, we can put ownership of the &lt;code&gt;CancellationTokenSource&lt;/code&gt; into a dedicated helper class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Helper Class
&lt;/h2&gt;

&lt;p&gt;The policy for this class is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Return only CancellationToken to the caller
- Let the helper class create, replace, cancel, and dispose CTS instances
- Do not leave a disposed CTS in the field
- End the old CTS when Renew / RenewOwned is called
- Dispose CTS instances created by CancelAfter and CreateLinkedTokenSource
- By default, do not throw ObjectDisposedException / AggregateException from Cancel()
- If onCancelException is specified, report the exception there before swallowing it
- Allow a strict mode that propagates Cancel exceptions when needed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Cancel()&lt;/code&gt; may throw &lt;code&gt;AggregateException&lt;/code&gt; if a registered cancellation callback throws.&lt;/p&gt;

&lt;p&gt;If an old CTS throws during &lt;code&gt;Cancel()&lt;/code&gt; inside &lt;code&gt;RenewOwned()&lt;/code&gt;, the caller sees &lt;code&gt;RenewOwned()&lt;/code&gt; as failed.&lt;/p&gt;

&lt;p&gt;However, depending on the implementation order, the new CTS may already have become active internally.&lt;/p&gt;

&lt;p&gt;For this reason, this helper class does not propagate exceptions caused by &lt;code&gt;Cancel()&lt;/code&gt; by default. If &lt;code&gt;onCancelException&lt;/code&gt; is specified, it reports the exception there.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;onCancelException&lt;/code&gt; is not specified, the exception is simply swallowed. In a real project, it is recommended to pass a logging function such as &lt;code&gt;Debug.LogException&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#nullable enable
&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Threading&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;CancellationTokenSourceOwner&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDisposable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;_gate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_throwOnCancelException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;_onCancelException&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;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_cts&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;_disposed&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;CancellationTokenSourceOwner&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;throwOnCancelException&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;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;onCancelException&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;_throwOnCancelException&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;throwOnCancelException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_onCancelException&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;onCancelException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="nf"&gt;Renew&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;RenewOwned&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;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="nf"&gt;RenewAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;delay&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;next&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;CancellationTokenSource&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;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CancelAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;RenewOwned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="nf"&gt;RenewLinked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;params&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;tokens&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;next&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateLinkedTokenSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&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;RenewOwned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="nf"&gt;RenewLinkedAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;params&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;tokens&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;next&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateLinkedTokenSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&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;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CancelAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;RenewOwned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Takes ownership of next.&lt;/span&gt;
    &lt;span class="c1"&gt;/// After calling this method, the caller must not dispose next.&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="nf"&gt;RenewOwned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt; &lt;span class="n"&gt;next&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;next&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_gate&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;_disposed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ObjectDisposedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationTokenSourceOwner&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_cts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;_cts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nf"&gt;CancelAndDisposeCore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&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;token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CancelAndDispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_gate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_cts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;_cts&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;CancelAndDisposeCore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_gate&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;_disposed&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;_disposed&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;old&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_cts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;_cts&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;CancelAndDisposeCore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&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;CancelAndDisposeCore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cts&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;cts&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="k"&gt;try&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;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ShouldHandleCancelException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;ReportCancelException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;ShouldHandleCancelException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_throwOnCancelException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;ObjectDisposedException&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;AggregateException&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;ReportCancelException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_onCancelException&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;handler&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="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Do not let logging failures break cleanup.&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;h2&gt;
  
  
  Implementation Notes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  RenewOwned Transfers Ownership
&lt;/h3&gt;

&lt;p&gt;The actual CTS replacement logic is centralized in &lt;code&gt;RenewOwned()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Renew()&lt;/code&gt;, &lt;code&gt;RenewAfter()&lt;/code&gt;, &lt;code&gt;RenewLinked()&lt;/code&gt;, and &lt;code&gt;RenewLinkedAfter()&lt;/code&gt; are convenience methods for different use cases, and they all call &lt;code&gt;RenewOwned()&lt;/code&gt; internally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;next&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;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CancelAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_owner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RenewOwned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, ownership of &lt;code&gt;next&lt;/code&gt; moves to &lt;code&gt;_owner&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The caller must not call &lt;code&gt;next.Dispose()&lt;/code&gt; after passing it to &lt;code&gt;RenewOwned()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Change State Inside the Lock, Cancel Outside the Lock
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;RenewOwned()&lt;/code&gt;, the class removes the old CTS and activates the new CTS inside the lock.&lt;/p&gt;

&lt;p&gt;However, &lt;code&gt;Cancel()&lt;/code&gt; itself is called outside the lock.&lt;/p&gt;

&lt;p&gt;The reason is that &lt;code&gt;Cancel()&lt;/code&gt; may execute registered cancellation callbacks synchronously.&lt;/p&gt;

&lt;p&gt;If the owner lock is held while calling &lt;code&gt;Cancel()&lt;/code&gt;, the design can become complicated depending on what those callbacks do.&lt;/p&gt;

&lt;p&gt;The order is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RenewOwned:
    Activate the new CTS inside the lock
    Exit the lock
    Cancel and dispose the old CTS

CancelAndDispose / Dispose:
    Remove the active CTS inside the lock
    Exit the lock
    Cancel and dispose the removed CTS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is one important caveat.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RenewOwned()&lt;/code&gt; activates the new CTS before canceling the old CTS.&lt;/p&gt;

&lt;p&gt;That means this class does not support reentrant usage where a cancellation callback from the old token calls &lt;code&gt;Renew()&lt;/code&gt;, &lt;code&gt;CancelAndDispose()&lt;/code&gt;, or &lt;code&gt;Dispose()&lt;/code&gt; on the same owner.&lt;/p&gt;

&lt;p&gt;Cancellation callbacks should avoid manipulating the owner itself and should be kept to minimal notifications or logging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cancel Exceptions Are Not Propagated by Default
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CancelAndDisposeCore()&lt;/code&gt; always calls &lt;code&gt;Dispose()&lt;/code&gt; even if &lt;code&gt;Cancel()&lt;/code&gt; throws.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ShouldHandleCancelException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;ReportCancelException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, &lt;code&gt;ObjectDisposedException&lt;/code&gt; and &lt;code&gt;AggregateException&lt;/code&gt; caused by &lt;code&gt;Cancel()&lt;/code&gt; are not propagated.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;onCancelException&lt;/code&gt; is specified, the exception is reported there and then swallowed.&lt;/p&gt;

&lt;p&gt;In Unity, this lets you avoid throwing from cleanup code while still keeping a log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSourceOwner&lt;/span&gt; &lt;span class="n"&gt;_loadToken&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;CancellationTokenSourceOwner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;onCancelException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;ObjectDisposedException&lt;/code&gt; is reported, check whether the CTS passed to &lt;code&gt;RenewOwned(next)&lt;/code&gt; was disposed by the caller, or whether the same CTS is being shared outside the owner.&lt;/p&gt;

&lt;p&gt;If you want to treat cancellation callback exceptions as bugs, enable strict mode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSourceOwner&lt;/span&gt; &lt;span class="n"&gt;_loadToken&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;CancellationTokenSourceOwner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;throwOnCancelException&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;In strict mode, &lt;code&gt;RenewOwned()&lt;/code&gt; may throw even though the new CTS has already become active internally.&lt;/p&gt;

&lt;p&gt;For Unity loading and screen transition code, passing a logging function to &lt;code&gt;onCancelException&lt;/code&gt; and using the non-throwing default is usually easier to handle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;The basic method is &lt;code&gt;Renew()&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;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSourceOwner&lt;/span&gt; &lt;span class="n"&gt;_loadToken&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;CancellationTokenSourceOwner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;onCancelException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Reload&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;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_loadToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Renew&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OperationCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Cancellation is treated as a normal flow.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling &lt;code&gt;Renew()&lt;/code&gt; creates a new CTS. If an old CTS exists, it is canceled and then disposed.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;RenewAfter()&lt;/code&gt; when you need a timeout.&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;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_loadToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RenewAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&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;RenewLinked()&lt;/code&gt; when you need to link with an external token.&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;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_loadToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RenewLinked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;destroyCancellationToken&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;RenewLinkedAfter()&lt;/code&gt; when you need both a timeout and an external token.&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;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_loadToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RenewLinkedAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;destroyCancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This cancels when either the &lt;code&gt;MonoBehaviour&lt;/code&gt; is destroyed or 10 seconds pass.&lt;/p&gt;

&lt;p&gt;Internally, this uses both &lt;code&gt;CreateLinkedTokenSource()&lt;/code&gt; and &lt;code&gt;CancelAfter()&lt;/code&gt;, but disposal of the CTS remains inside &lt;code&gt;CancellationTokenSourceOwner&lt;/code&gt;.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_loadToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CancelAndDispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnDestroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_loadToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CancelAndDispose()&lt;/code&gt; removes the active CTS first, then calls &lt;code&gt;Cancel()&lt;/code&gt; and &lt;code&gt;Dispose()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So even if it is called multiple times, the second and later calls have no active CTS to touch.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Dispose()&lt;/code&gt; ends the lifetime of the owner itself. Calling &lt;code&gt;Renew()&lt;/code&gt; after &lt;code&gt;Dispose()&lt;/code&gt; throws &lt;code&gt;ObjectDisposedException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;CancelAndDispose()&lt;/code&gt; when you want to stop the current operation but keep the owner reusable. Use &lt;code&gt;Dispose()&lt;/code&gt; when the owner itself is done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining with destroyCancellationToken
&lt;/h2&gt;

&lt;p&gt;Unity 2022.2 and later provides &lt;code&gt;MonoBehaviour.destroyCancellationToken&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;destroyCancellationToken&lt;/code&gt; is managed by Unity, so you do not dispose it yourself.&lt;/p&gt;

&lt;p&gt;However, a linked CTS created by &lt;code&gt;CreateLinkedTokenSource()&lt;/code&gt; is created by your code, so it must be disposed.&lt;/p&gt;

&lt;p&gt;When using this helper class, the mental model becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;destroyCancellationToken
    Token managed by Unity
    Do not dispose it yourself

RenewLinked(destroyCancellationToken)
    Owner creates a linked CTS
    Owner disposes it

RenewLinkedAfter(timeout, destroyCancellationToken)
    Owner creates a linked CTS
    Owner sets CancelAfter
    Owner disposes it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CancellationTokenSourceOwner&lt;/code&gt; is not a universal cancellation management system.&lt;/p&gt;

&lt;p&gt;Use it with the following assumptions in mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Not Reenter the Owner from Cancellation Callbacks
&lt;/h3&gt;

&lt;p&gt;This class does not support calling &lt;code&gt;Renew()&lt;/code&gt;, &lt;code&gt;CancelAndDispose()&lt;/code&gt;, or &lt;code&gt;Dispose()&lt;/code&gt; on the same owner from a cancellation callback of the old token.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RenewOwned()&lt;/code&gt; activates the new CTS before canceling the old CTS.&lt;/p&gt;

&lt;p&gt;If a callback from the old token manipulates the owner, it may affect the new CTS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Renew / CancelAndDispose May Be Synchronously Expensive
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Cancel()&lt;/code&gt; may run cancellation callbacks synchronously.&lt;/p&gt;

&lt;p&gt;That means &lt;code&gt;Renew()&lt;/code&gt;, &lt;code&gt;CancelAndDispose()&lt;/code&gt;, and &lt;code&gt;Dispose()&lt;/code&gt; may not return until callbacks of the old CTS have finished.&lt;/p&gt;

&lt;p&gt;If you call these methods on the Unity main thread, heavy cancellation callbacks can cause frame drops.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Old CTS Is Disposed Immediately After Cancel
&lt;/h3&gt;

&lt;p&gt;This class disposes the old CTS immediately after calling &lt;code&gt;Cancel()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For normal &lt;code&gt;async&lt;/code&gt; / &lt;code&gt;await&lt;/code&gt; code and APIs that accept &lt;code&gt;CancellationToken&lt;/code&gt;, this is usually convenient.&lt;/p&gt;

&lt;p&gt;However, it does not work well with code that touches &lt;code&gt;token.WaitHandle&lt;/code&gt; after cancellation.&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;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_loadToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Renew&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;_loadToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CancelAndDispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Not recommended:&lt;/span&gt;
&lt;span class="c1"&gt;// The old CTS has already been disposed, so this design does not work well with WaitHandle.&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;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitHandle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you pass tokens to old APIs or custom implementations that use &lt;code&gt;WaitHandle&lt;/code&gt;, consider a design where the CTS is disposed only after the operation has fully completed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Always Handle Exceptions from async void
&lt;/h3&gt;

&lt;p&gt;Unity event handlers sometimes use &lt;code&gt;async void&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, exceptions escaping from &lt;code&gt;async void&lt;/code&gt; are hard to handle, so it is safer to explicitly catch both &lt;code&gt;OperationCanceledException&lt;/code&gt; and normal exceptions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OperationCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Cancellation is normal flow.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outside Unity event handlers, prefer &lt;code&gt;async Task&lt;/code&gt; when possible so the caller can await the operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CancellationTokenSource&lt;/code&gt; is not finished just because you called &lt;code&gt;Cancel()&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;Cancel()
    Sends a cancellation request

Dispose()
    Releases resources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are separate responsibilities.&lt;/p&gt;

&lt;p&gt;Especially when using &lt;code&gt;CancelAfter()&lt;/code&gt; or &lt;code&gt;CreateLinkedTokenSource()&lt;/code&gt;, failing to dispose the CTS after use may delay the release of internal resources and registrations.&lt;/p&gt;

&lt;p&gt;Also, when keeping CTS directly in fields, it is easy to leave a disposed CTS behind, call &lt;code&gt;Cancel()&lt;/code&gt; on an already disposed CTS, or forget to dispose CTS instances created for timeouts or linked tokens.&lt;/p&gt;

&lt;p&gt;Putting ownership of the CTS into a dedicated class makes the code easier to reason about.&lt;/p&gt;

&lt;p&gt;The caller receives only a &lt;code&gt;CancellationToken&lt;/code&gt;, while creation, cancellation, disposal, renewal, linked tokens, and timeouts are handled by the owner class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If you create a CancellationTokenSource yourself, you are responsible for disposing it.
But instead of repeating that logic in every class, put ownership in one place.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this structure, Unity loading and networking code becomes easier to handle safely.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>unity3d</category>
      <category>canellationtokensource</category>
    </item>
    <item>
      <title>Do We Still Need UniTask in the Unity 6 Era? Choosing Between Awaitable and UniTask</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Wed, 24 Jun 2026 17:00:32 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/do-we-still-need-unitask-in-the-unity-6-era-choosing-between-awaitable-and-unitask-5d6</link>
      <guid>https://dev.to/gamedevtoollab/do-we-still-need-unitask-in-the-unity-6-era-choosing-between-awaitable-and-unitask-5d6</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;When writing asynchronous code in Unity, &lt;code&gt;Coroutine&lt;/code&gt; and &lt;code&gt;UniTask&lt;/code&gt; have long been the practical choices.&lt;/p&gt;

&lt;p&gt;A major reason is that many Unity APIs must be called from the main thread.&lt;/p&gt;

&lt;p&gt;If you simply use &lt;code&gt;.NET Task&lt;/code&gt; and move work to a background thread, you can run into problems the moment you touch objects such as &lt;code&gt;GameObject&lt;/code&gt;, &lt;code&gt;Transform&lt;/code&gt;, or &lt;code&gt;SceneManager&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;UniTask has been a very convenient solution to this constraint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SwitchToThreadPool&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Heavy computation, JSON parsing, binary parsing, etc.&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;HeavyWork&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SwitchToMainThread&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Touch Unity APIs&lt;/span&gt;
&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;Name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, starting with Unity 2023.1, Unity introduced the standard &lt;code&gt;Awaitable&lt;/code&gt; API.&lt;/p&gt;

&lt;p&gt;In Unity 6, APIs such as &lt;code&gt;Awaitable.NextFrameAsync()&lt;/code&gt;, &lt;code&gt;Awaitable.WaitForSecondsAsync()&lt;/code&gt;, &lt;code&gt;Awaitable.BackgroundThreadAsync()&lt;/code&gt;, and &lt;code&gt;Awaitable.MainThreadAsync()&lt;/code&gt; are available.&lt;/p&gt;

&lt;p&gt;In other words, Unity now provides a standard way to write a certain amount of &lt;code&gt;async/await&lt;/code&gt; code.&lt;/p&gt;

&lt;p&gt;So, in the Unity 6 era, do we still need UniTask?&lt;/p&gt;

&lt;p&gt;This article is not a basic UniTask tutorial. Instead, it focuses on &lt;strong&gt;what changed with the arrival of Awaitable&lt;/strong&gt;, and &lt;strong&gt;where UniTask still remains valuable&lt;/strong&gt;.&lt;/p&gt;




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

&lt;p&gt;Here is the conclusion first.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;With the arrival of Awaitable, Unity is no longer in a world where "UniTask is the only realistic way to use async/await."&lt;br&gt;&lt;br&gt;
However, if you want to operate asynchronous workflows properly in an actual game application, UniTask is still very useful.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A rough breakdown looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Recommended choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Waiting until the next frame&lt;/td&gt;
&lt;td&gt;Awaitable is often enough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Waiting for a few seconds&lt;/td&gt;
&lt;td&gt;Awaitable is often enough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lightweight coroutine replacement&lt;/td&gt;
&lt;td&gt;Awaitable is often enough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Library code where you want to avoid external dependencies&lt;/td&gt;
&lt;td&gt;Returning Awaitable can be a good fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Waiting for multiple async operations together&lt;/td&gt;
&lt;td&gt;UniTask is stronger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large-scale loading, networking, Addressables control&lt;/td&gt;
&lt;td&gt;UniTask is stronger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Practical cancellation handling&lt;/td&gt;
&lt;td&gt;UniTask is stronger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fine-grained PlayerLoopTiming control&lt;/td&gt;
&lt;td&gt;UniTask is stronger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tracking leaked or forgotten async operations&lt;/td&gt;
&lt;td&gt;UniTask is stronger&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Very roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Small async code: Awaitable
Application-wide async infrastructure: UniTask
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the mental model I use.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changed with Awaitable?
&lt;/h2&gt;

&lt;p&gt;The biggest change is that Unity now supports code like this as a standard feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BackgroundThreadAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Now on a ThreadPool thread&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;HeavyWork&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MainThreadAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Back on the Unity main thread&lt;/span&gt;
&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;Name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a big deal.&lt;/p&gt;

&lt;p&gt;Previously, even if you wanted to use &lt;code&gt;async/await&lt;/code&gt; in Unity, using &lt;code&gt;.NET Task&lt;/code&gt; directly often did not fit well with Unity's main-thread restrictions.&lt;/p&gt;

&lt;p&gt;You can move work to a background thread with &lt;code&gt;Task.Run&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;HeavyWork&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, touching Unity APIs inside that code is dangerous.&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;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// NG&lt;/span&gt;
    &lt;span class="k"&gt;return&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Many Unity APIs are designed to be used from the main thread.&lt;/p&gt;

&lt;p&gt;Because of that, you need a design that explicitly returns to the main thread before touching Unity APIs.&lt;/p&gt;

&lt;p&gt;UniTask made this very natural.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SwitchToThreadPool&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;HeavyWork&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SwitchToMainThread&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;ApplyResultToUnityObject&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Today, Unity's standard Awaitable API can express the same basic idea.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BackgroundThreadAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;HeavyWork&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MainThreadAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;ApplyResultToUnityObject&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means that &lt;strong&gt;introducing UniTask only to switch back to the main thread is less necessary than it used to be&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Task and Awaitable also differ in continuation timing
&lt;/h2&gt;

&lt;p&gt;The arrival of Awaitable did not just add more APIs that can be used with &lt;code&gt;async/await&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.NET Task&lt;/code&gt; and Unity's &lt;code&gt;Awaitable&lt;/code&gt; also differ in how continuations are handled.&lt;/p&gt;

&lt;p&gt;In Unity, a continuation from a &lt;code&gt;Task&lt;/code&gt; called on the main thread can be posted to &lt;code&gt;UnitySynchronizationContext&lt;/code&gt;, and may resume on the next &lt;code&gt;Update&lt;/code&gt; tick.&lt;/p&gt;

&lt;p&gt;On the other hand, Unity's &lt;code&gt;Awaitable&lt;/code&gt; is designed to run continuations synchronously when the operation completes.&lt;/p&gt;

&lt;p&gt;That means that when dealing with Unity-provided asynchronous operations, Awaitable is designed to avoid unnecessary one-frame delays more easily.&lt;/p&gt;

&lt;p&gt;This does not mean "Awaitable is always faster."&lt;/p&gt;

&lt;p&gt;But it does mean that Awaitable is designed around Unity's frame loop, while &lt;code&gt;Task&lt;/code&gt; is a general-purpose .NET abstraction.&lt;/p&gt;

&lt;p&gt;That distinction is worth keeping in mind.&lt;/p&gt;




&lt;h2&gt;
  
  
  await is not magic that automatically moves work to another thread
&lt;/h2&gt;

&lt;p&gt;This is an easy misunderstanding.&lt;/p&gt;

&lt;p&gt;Writing &lt;code&gt;await&lt;/code&gt; does not automatically move the entire operation to a background thread.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BackgroundThreadAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means that the following continuation resumes on a background thread.&lt;/p&gt;

&lt;p&gt;Conversely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MainThreadAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means that the following continuation resumes on the Unity main thread.&lt;/p&gt;

&lt;p&gt;So the following code is dangerous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BackgroundThreadAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// NG: touching Unity APIs from a background thread&lt;/span&gt;
&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Loaded"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The correct pattern is to return to the main thread before touching Unity APIs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BackgroundThreadAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;HeavyWork&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MainThreadAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;Name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same idea applies to UniTask.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SwitchToThreadPool&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;HeavyWork&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SwitchToMainThread&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;Name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Awaitable does not remove Unity's main-thread restriction.&lt;/p&gt;

&lt;p&gt;What changed is that Unity now provides standard APIs to work with that restriction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Awaitable is very convenient as a coroutine replacement
&lt;/h2&gt;

&lt;p&gt;Awaitable can replace many common coroutine patterns quite naturally.&lt;/p&gt;

&lt;p&gt;Waiting until the next frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NextFrameAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Waiting for a fixed amount of time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitForSecondsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Waiting for the FixedUpdate timing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FixedUpdateAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Waiting until the end of the frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndOfFrameAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For lightweight use cases, this is often enough.&lt;/p&gt;

&lt;p&gt;For example, UI effects, simple waits, short samples, and small libraries that want to avoid external dependencies can often use Awaitable without bringing in UniTask.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt; &lt;span class="nf"&gt;ShowMessageAsync&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;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitForSecondsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this level of code, UniTask is not mandatory.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Awaitable replaced, and what it did not
&lt;/h2&gt;

&lt;p&gt;Awaitable did not erase all of UniTask's value.&lt;/p&gt;

&lt;p&gt;However, some of UniTask's previous value became less unique.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Writing next-frame waits with async/await
Writing WaitForSeconds-like waits with async/await
Writing EndOfFrame-like waits with async/await
Returning to the main thread using Unity-standard APIs
Moving to a background thread using Unity-standard APIs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These can now be written fairly naturally with Awaitable.&lt;/p&gt;

&lt;p&gt;On the other hand, UniTask still has strong value in areas 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;WhenAll / WhenAny / WhenEach
Fine-grained PlayerLoopTiming control
Frame-based APIs such as DelayFrame
Unity-oriented CancellationToken workflows
UniTaskTracker
Integration with Addressables, DOTween, uGUI, and other surrounding libraries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the value that became less unique is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Making async/await usable in Unity at all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value that remains is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Making asynchronous workflows easier to operate across a full application
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you separate these two ideas, the decision becomes much clearer.&lt;/p&gt;




&lt;h2&gt;
  
  
  So where is UniTask still strong?
&lt;/h2&gt;

&lt;p&gt;If Awaitable can switch threads, does that mean UniTask lost its value?&lt;/p&gt;

&lt;p&gt;Personally, I see it less as "UniTask lost value" and more as "UniTask's role shifted."&lt;/p&gt;

&lt;p&gt;Previously, UniTask often served as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The practical foundation for using async/await in Unity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After Awaitable, its role is closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A practical foundation for operating larger asynchronous workflows in Unity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference becomes clear in the following areas.&lt;/p&gt;




&lt;h2&gt;
  
  
  UniTask strength 1: WhenAll / WhenAny / WhenEach are practical
&lt;/h2&gt;

&lt;p&gt;In real loading code, a single async operation is rarely the whole story.&lt;/p&gt;

&lt;p&gt;You often want to start multiple operations, such as loading master data, Addressables assets, textures, or network responses, and then wait for all of them.&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;playerTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;LoadPlayerAsync&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;itemTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;LoadItemAsync&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;questTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;LoadQuestAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;itemTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;questTask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Receiving multiple results is also clean.&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;LoadPlayerAsync&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;LoadItemAsync&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;LoadQuestAsync&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 very useful in practice.&lt;/p&gt;

&lt;p&gt;If you await each operation one by one, they become sequential.&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;player&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadPlayerAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadItemAsync&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;quest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadQuestAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this form, &lt;code&gt;LoadItemAsync&lt;/code&gt; does not start until &lt;code&gt;LoadPlayerAsync&lt;/code&gt; has finished.&lt;/p&gt;

&lt;p&gt;If you start all operations first and then wait for them together, operations that can run concurrently will proceed concurrently.&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;playerTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;LoadPlayerAsync&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;itemTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;LoadItemAsync&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;questTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;LoadQuestAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;playerTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;itemTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;questTask&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In game loading code, this can directly affect loading time.&lt;/p&gt;

&lt;p&gt;If you want to process results as they complete, &lt;code&gt;WhenEach&lt;/code&gt; is also useful.&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;tasks&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="nf"&gt;LoadMasterAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"player"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;LoadMasterAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;LoadMasterAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"quest"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tasks&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;master&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="nf"&gt;GetResult&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;ApplyMaster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WhenEach&lt;/code&gt; is useful when you want to apply results in completion order instead of waiting for everything first.&lt;/p&gt;

&lt;p&gt;If you want to handle errors individually, you can inspect each result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tasks&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsFaulted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;ApplyMaster&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="nf"&gt;GetResult&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 where Awaitable's characteristics matter.&lt;/p&gt;

&lt;p&gt;Awaitable instances are pooled internally, and you should not await the same Awaitable instance more than once.&lt;/p&gt;

&lt;p&gt;Awaiting the same instance multiple times can lead to exceptions, deadlocks, or behavior close to undefined behavior.&lt;/p&gt;

&lt;p&gt;Also, standard Awaitable does not provide the same level of composition APIs as UniTask's &lt;code&gt;WhenAll&lt;/code&gt;, &lt;code&gt;WhenAny&lt;/code&gt;, and &lt;code&gt;WhenEach&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So Awaitable is convenient for lightweight coroutine replacement, but UniTask is more practical when you need to manage many loading operations or network requests together.&lt;/p&gt;




&lt;h2&gt;
  
  
  UniTask strength 2: Fine-grained PlayerLoopTiming control
&lt;/h2&gt;

&lt;p&gt;In Unity async code, simply returning to the main thread is not always enough.&lt;/p&gt;

&lt;p&gt;Sometimes you need to care about where in the PlayerLoop you resume.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Resume before Update
Resume after LateUpdate
Run at FixedUpdate timing
Run at an EndOfFrame-like timing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;UniTask lets you specify &lt;code&gt;PlayerLoopTiming&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;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerLoopTiming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerLoopTiming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastPostLateUpdate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also specify timing when switching back to the main thread.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SwitchToMainThread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerLoopTiming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PreLateUpdate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can matter for code 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;UI updates
Camera post-processing
Code that depends on Transform updates
Loading progress display
Screenshots at the end of the frame
Post-processing after Addressables or AsyncOperation completion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Awaitable has &lt;code&gt;NextFrameAsync&lt;/code&gt;, &lt;code&gt;FixedUpdateAsync&lt;/code&gt;, and &lt;code&gt;EndOfFrameAsync&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, it does not expose PlayerLoopTiming as finely as UniTask.&lt;/p&gt;

&lt;p&gt;UniTask's &lt;code&gt;PlayerLoopTiming&lt;/code&gt; includes many phases such as &lt;code&gt;Initialization&lt;/code&gt;, &lt;code&gt;EarlyUpdate&lt;/code&gt;, &lt;code&gt;FixedUpdate&lt;/code&gt;, &lt;code&gt;PreUpdate&lt;/code&gt;, &lt;code&gt;Update&lt;/code&gt;, &lt;code&gt;PreLateUpdate&lt;/code&gt;, &lt;code&gt;PostLateUpdate&lt;/code&gt;, and &lt;code&gt;LastPostLateUpdate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the practical distinction is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If returning to the main thread is enough: Awaitable can be enough
If the exact PlayerLoop timing matters: UniTask is stronger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  UniTask strength 3: Yield and NextFrame can be used intentionally
&lt;/h2&gt;

&lt;p&gt;A subtle but important UniTask detail is the difference between &lt;code&gt;Yield&lt;/code&gt; and &lt;code&gt;NextFrame&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;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Yield&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NextFrame&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 mean exactly the same thing.&lt;/p&gt;

&lt;p&gt;If your intent is "wait until the next frame" in the same sense as &lt;code&gt;yield return null&lt;/code&gt;, then &lt;code&gt;UniTask.NextFrame()&lt;/code&gt; usually expresses that intent more clearly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NextFrame&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the other hand, &lt;code&gt;UniTask.Yield()&lt;/code&gt; resumes at the specified PlayerLoopTiming.&lt;/p&gt;

&lt;p&gt;Depending on when it is called, it may resume within the same frame.&lt;/p&gt;

&lt;p&gt;So if you casually use &lt;code&gt;Yield&lt;/code&gt; as a replacement for &lt;code&gt;yield return null&lt;/code&gt;, it can cause bugs in code that depends on frame boundaries.&lt;/p&gt;

&lt;p&gt;This is easy to miss in basic tutorials.&lt;/p&gt;

&lt;p&gt;In asynchronous code, it is not enough to say "wait."&lt;br&gt;&lt;br&gt;
Sometimes it matters &lt;strong&gt;when the continuation resumes&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  UniTask strength 4: Cancellation is easier to operate in practice
&lt;/h2&gt;

&lt;p&gt;In games, an async operation often outlives the object that started it.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The scene changed
The screen was closed
The prefab was destroyed
A network request was canceled
The player returned to the title screen during loading
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the async operation keeps running, it may later try to touch an object that has already been destroyed.&lt;/p&gt;

&lt;p&gt;UniTask makes common Unity cancellation patterns easy to write.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;UniTaskVoid&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCancellationTokenOnDestroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OperationCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Cancellation caused by Destroy, screen transition, etc.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Touch Unity APIs&lt;/span&gt;
    &lt;span class="n"&gt;label&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;"Loaded"&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;Awaitable APIs can also accept &lt;code&gt;CancellationToken&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, UniTask provides many practical patterns that fit Unity development very well.&lt;/p&gt;

&lt;p&gt;In particular, &lt;code&gt;GetCancellationTokenOnDestroy()&lt;/code&gt; makes it easy to tie the lifetime of an async operation to the lifetime of a &lt;code&gt;MonoBehaviour&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  UniTask strength 5: UniTaskTracker helps find forgotten operations
&lt;/h2&gt;

&lt;p&gt;Asynchronous operations are easy to leave behind invisibly.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;SomeAsync&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Forget&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;Forget()&lt;/code&gt; is convenient, but if used carelessly, the operation may continue running even after the screen has closed.&lt;/p&gt;

&lt;p&gt;Loading, networking, polling, and UI effects can keep running and produce mysterious logs or exceptions later.&lt;/p&gt;

&lt;p&gt;UniTask includes &lt;code&gt;UniTaskTracker&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is very useful in practice.&lt;/p&gt;

&lt;p&gt;Async problems are not always about the speed of a single operation.&lt;/p&gt;

&lt;p&gt;Often the real issue is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;An operation that should have ended is still running
An operation that should have been canceled is still active
The same async operation was started multiple times
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Awaitable does not have an equivalent tracker.&lt;/p&gt;

&lt;p&gt;This is another reason UniTask remains strong as an application-wide async foundation.&lt;/p&gt;




&lt;h2&gt;
  
  
  A common allocation trap around Progress
&lt;/h2&gt;

&lt;p&gt;Even if you use UniTask, surrounding code can still allocate.&lt;/p&gt;

&lt;p&gt;A typical example is &lt;code&gt;Progress&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;progress&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;Progress&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&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;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;progressBar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is convenient, but if created frequently, it can become a source of allocations.&lt;/p&gt;

&lt;p&gt;UniTask provides &lt;code&gt;Progress.Create&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;progress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Progress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&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;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;progressBar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For very high-frequency progress reporting, you can also implement &lt;code&gt;IProgress&amp;lt;T&amp;gt;&lt;/code&gt; yourself and reuse the instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProgressBarReporter&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IProgress&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&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;readonly&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;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Slider&lt;/span&gt; &lt;span class="n"&gt;slider&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;ProgressBarReporter&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;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Slider&lt;/span&gt; &lt;span class="n"&gt;slider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slider&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;Report&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;slider&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;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;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;progress&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;ProgressBarReporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;progressBar&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you see allocations while using UniTask, the cause may not be UniTask itself.&lt;/p&gt;

&lt;p&gt;It may be &lt;code&gt;Progress&lt;/code&gt;, lambdas, closures, LINQ, array allocations, or other surrounding code.&lt;/p&gt;

&lt;p&gt;So it is important to look at the whole implementation, not just whether UniTask is being used.&lt;/p&gt;




&lt;h2&gt;
  
  
  Moving to the ThreadPool does not automatically make loading faster
&lt;/h2&gt;

&lt;p&gt;This is another common misunderstanding.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BackgroundThreadAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SwitchToThreadPool&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can move heavy work to a background thread.&lt;/p&gt;

&lt;p&gt;However, Unity loading itself does not necessarily become fully background-threaded just because you moved your continuation.&lt;/p&gt;

&lt;p&gt;For example, Addressables, AssetBundle loading, scene loading, texture creation, and mesh creation may still include main-thread work internally.&lt;/p&gt;

&lt;p&gt;So you should not assume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I moved to BackgroundThread, so all Unity loading no longer blocks the main thread.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Moving to a background thread is most effective for work that does not depend on Unity APIs.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JSON parsing
MessagePack parsing
Custom binary parsing
Encryption / decryption
Compression / decompression
Hash calculation
Large array processing
Pure computation such as pathfinding or scoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the other hand, creating &lt;code&gt;UnityEngine.Object&lt;/code&gt;, touching &lt;code&gt;Transform&lt;/code&gt;, or loading scenes still requires main-thread work at some point.&lt;/p&gt;

&lt;p&gt;This boundary matters.&lt;/p&gt;

&lt;p&gt;Also, if you target WebGL or web builds, be careful with designs that depend on ThreadPool.&lt;/p&gt;

&lt;p&gt;The UniTask README explains that APIs such as &lt;code&gt;UniTask.SwitchToThreadPool()&lt;/code&gt; and &lt;code&gt;UniTask.RunOnThreadPool()&lt;/code&gt; do not work on WebGL and similar environments.&lt;/p&gt;

&lt;p&gt;If web builds are part of your target, avoid depending too heavily on ThreadPool-based designs. Instead, consider splitting work across frames, using coroutine or PlayerLoop-based distribution, or checking the current state of Job System support for your target.&lt;/p&gt;




&lt;h2&gt;
  
  
  A safer pattern for using ThreadPool with Awaitable
&lt;/h2&gt;

&lt;p&gt;When using Awaitable for background work, pay attention to &lt;strong&gt;which thread the method completes on&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, the following code needs caution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ParsedData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ParseAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BackgroundThreadAsync&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;parsed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ParseBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;parsed&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 method does its work on a background thread and then completes there.&lt;/p&gt;

&lt;p&gt;If the caller immediately touches Unity APIs after awaiting it, that can be dangerous.&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;await&lt;/span&gt; &lt;span class="nf"&gt;ParseAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// If ParseAsync completes on a background thread,&lt;/span&gt;
&lt;span class="c1"&gt;// touching Unity APIs here is dangerous.&lt;/span&gt;
&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;Name&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 caller may touch Unity APIs after this method returns, a safer default is to return to the main thread before completing the method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ParsedData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ParseAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;bytes&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;continueOnMainThread&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BackgroundThreadAsync&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;parsed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ParseBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;continueOnMainThread&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MainThreadAsync&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;parsed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this pattern, normal calls complete after returning to the main thread.&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;await&lt;/span&gt; &lt;span class="nf"&gt;ParseAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ParseAsync has returned to the main thread before completion,&lt;/span&gt;
&lt;span class="c1"&gt;// so touching Unity APIs is easier to reason about.&lt;/span&gt;
&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;Name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to chain multiple heavy operations on a background thread, you can opt out explicitly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="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;await&lt;/span&gt; &lt;span class="nf"&gt;ParseAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;continueOnMainThread&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="c1"&gt;// If you want to touch Unity APIs here,&lt;/span&gt;
&lt;span class="c1"&gt;// explicitly return to the main thread.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MainThreadAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;Name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important point is to make the contract clear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Which thread does this async method complete on?
Is it safe to touch Unity APIs after awaiting it?
How far does cancellation propagate?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, Unity does not necessarily stop background operations automatically when exiting Play Mode.&lt;/p&gt;

&lt;p&gt;When using background work in the Editor, consider using &lt;code&gt;Application.exitCancellationToken&lt;/code&gt; so the operation can be canceled when Play Mode ends.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using RunOnThreadPool with UniTask
&lt;/h2&gt;

&lt;p&gt;With UniTask, you can also write background execution that returns a result 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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunOnThreadPool&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ParseBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;ApplyToUnityObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same warning applies here.&lt;/p&gt;

&lt;p&gt;Do not touch Unity APIs inside &lt;code&gt;ParseBytes&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;UniTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunOnThreadPool&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// NG&lt;/span&gt;
    &lt;span class="kt"&gt;var&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ParseBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code running on the ThreadPool should be limited to pure work that does not depend on Unity APIs.&lt;/p&gt;




&lt;h2&gt;
  
  
  For library development, returning Awaitable can make sense
&lt;/h2&gt;

&lt;p&gt;For application code, UniTask is still very useful.&lt;/p&gt;

&lt;p&gt;However, for libraries or Unity Asset Store packages, you may want to avoid adding external dependencies.&lt;/p&gt;

&lt;p&gt;If you can target Unity 6 or later, returning &lt;code&gt;Awaitable&lt;/code&gt; can be a realistic choice.&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;async&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NextFrameAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Work&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 application uses UniTask, it can adapt Awaitable as needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Library side: return Awaitable
Application side: convert or wrap it for UniTask-based operation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the library dependency-light while still allowing the application to use UniTask's operational features.&lt;/p&gt;

&lt;p&gt;UniTask can handle Awaitable with &lt;code&gt;AsUniTask()&lt;/code&gt;, so the two do not need to be treated as mutually exclusive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical guideline
&lt;/h2&gt;

&lt;p&gt;Here is a practical rule of thumb.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Awaitable&lt;/th&gt;
&lt;th&gt;UniTask&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small waiting logic&lt;/td&gt;
&lt;td&gt;Good fit&lt;/td&gt;
&lt;td&gt;Also possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lightweight coroutine replacement&lt;/td&gt;
&lt;td&gt;Good fit&lt;/td&gt;
&lt;td&gt;Also possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard Unity-only implementation&lt;/td&gt;
&lt;td&gt;Good fit&lt;/td&gt;
&lt;td&gt;Adds external dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Library return type&lt;/td&gt;
&lt;td&gt;Good fit&lt;/td&gt;
&lt;td&gt;Requires users to depend on UniTask&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Waiting for multiple loads together&lt;/td&gt;
&lt;td&gt;Weak&lt;/td&gt;
&lt;td&gt;Good fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managing many async operations&lt;/td&gt;
&lt;td&gt;Weak&lt;/td&gt;
&lt;td&gt;Good fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Designing cancellation across systems&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Good fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fine-grained PlayerLoopTiming control&lt;/td&gt;
&lt;td&gt;Weak&lt;/td&gt;
&lt;td&gt;Good fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finding forgotten async operations&lt;/td&gt;
&lt;td&gt;Weak&lt;/td&gt;
&lt;td&gt;Good fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Considering WebGL / ThreadPool limitations&lt;/td&gt;
&lt;td&gt;Be careful&lt;/td&gt;
&lt;td&gt;Be careful&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important point is that Awaitable and UniTask are not enemies.&lt;/p&gt;

&lt;p&gt;Awaitable is Unity's lightweight standard async API.&lt;/p&gt;

&lt;p&gt;UniTask is a practical tool for managing larger async workflows in Unity applications.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Awaitable made asynchronous programming in Unity much more practical.&lt;/p&gt;

&lt;p&gt;The biggest changes are that Unity now provides standard APIs for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Waiting until the next frame
Waiting for seconds
Waiting for FixedUpdate
Waiting until EndOfFrame
Awaiting AsyncOperation
Switching to the main thread
Switching to a background thread
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because of this, simple coroutine replacements and small async workflows can often be written with Awaitable alone.&lt;/p&gt;

&lt;p&gt;However, real game development rarely stops at single async operations.&lt;/p&gt;

&lt;p&gt;You often need to handle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Large-scale loading
Networking
Addressables
Cancellation
Screen transitions
Destroy-linked lifetimes
Exception handling
Progress reporting
PlayerLoopTiming control
Tracking forgotten operations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you consider these operational concerns, UniTask still has a strong reason to exist.&lt;/p&gt;

&lt;p&gt;The conclusion is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Awaitable made Unity-standard async/await much more realistic.
But if you need to operate asynchronous workflows in a real application, UniTask is still powerful.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Unity 6 era, UniTask is no longer just a coroutine replacement.&lt;/p&gt;

&lt;p&gt;It is better understood as a foundation for organizing asynchronous workflows across an application.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Unity Manual: Introduction to asynchronous programming with Awaitable&lt;br&gt;&lt;br&gt;
&lt;a href="https://docs.unity3d.com/6000.1/Documentation/Manual/async-awaitable-introduction.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/6000.1/Documentation/Manual/async-awaitable-introduction.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unity Manual: Awaitable completion and continuation&lt;br&gt;&lt;br&gt;
&lt;a href="https://docs.unity3d.com/6000.1/Documentation/Manual/async-awaitable-continuations.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/6000.1/Documentation/Manual/async-awaitable-continuations.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unity Manual: Awaitable completion and continuation (Unity 6000.0)&lt;br&gt;&lt;br&gt;
&lt;a href="https://docs.unity3d.com/6000.0/Documentation/Manual/async-awaitable-continuations.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/6000.0/Documentation/Manual/async-awaitable-continuations.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unity Scripting API: Awaitable&lt;br&gt;&lt;br&gt;
&lt;a href="https://docs.unity3d.com/6000.2/Documentation/ScriptReference/Awaitable.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/6000.2/Documentation/ScriptReference/Awaitable.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;UniTask GitHub README&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Cysharp/UniTask" rel="noopener noreferrer"&gt;https://github.com/Cysharp/UniTask&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>unitask</category>
      <category>awaitable</category>
    </item>
    <item>
      <title>Fixing Addressables Downloads That Keep Running After Cancellation</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Mon, 22 Jun 2026 13:16:20 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/fixing-addressables-downloads-that-keep-running-after-cancellation-47l5</link>
      <guid>https://dev.to/gamedevtoollab/fixing-addressables-downloads-that-keep-running-after-cancellation-47l5</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;When downloading a large number of remote assets with Unity Addressables, you can limit the number of concurrent network requests by setting &lt;code&gt;MaxConcurrentWebRequests&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, this only controls the &lt;strong&gt;number of concurrent requests&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, suppose you download 500 AssetBundles at once. Even if the number of concurrent requests is limited to 3, the remaining 497 requests are still added to Addressables' internal request queue.&lt;/p&gt;

&lt;p&gt;If the user presses a cancel button in this state, the queued requests inside Addressables are not automatically removed.&lt;/p&gt;

&lt;p&gt;Depending on your implementation, the flow can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start downloading 500 files
↓
3 requests start immediately
497 requests are queued in Addressables' internal WebRequestQueue
↓
The user cancels
↓
The app-side await / CancellationToken is treated as canceled
↓
However, the internal Addressables queue remains
↓
Every time a request slot becomes available, the next queued request starts
↓
Even after canceling, the app still waits for a large number of failed requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if you limit the number of concurrent downloads with &lt;code&gt;MaxConcurrentWebRequests&lt;/code&gt;, &lt;strong&gt;canceling can still take a very long time when the total number of downloads is large&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article introduces an approach where we directly modify the internal Addressables code so that both queued requests and active requests can be canceled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assumptions
&lt;/h2&gt;

&lt;p&gt;This article uses Addressables &lt;strong&gt;2.9.1&lt;/strong&gt; as the example.&lt;/p&gt;

&lt;p&gt;However, the internal implementation of Addressables can change depending on the version.&lt;/p&gt;

&lt;p&gt;So rather than copying the code in this article as-is, you should first check the following points in the version used by your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Where the queued web requests are stored
Where UnityWebRequest is started
Where completion is reported back to AssetBundleProvider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then adjust the implementation to match the Addressables version used in your project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why submit all requests first?
&lt;/h2&gt;

&lt;p&gt;If you only care about cancellation, the safest approach is to create your own queue on the app side and submit only a few requests to Addressables at a time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start only 3 downloads
↓
When one finishes, start the next one
↓
If canceled, do not submit the remaining requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this approach, cancellation is simple.&lt;/p&gt;

&lt;p&gt;However, in an actual game, you often want to display UI like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total download size: 1.2 GB
Remaining download size: 850 MB
Progress: 29%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By using &lt;code&gt;AsyncOperationHandle.GetDownloadStatus()&lt;/code&gt; in Addressables, you can obtain the downloaded byte count and the total byte count.&lt;/p&gt;

&lt;p&gt;Because of that, there are cases where you want to pass the entire download target set to Addressables at the beginning, so that the progress UI can be calculated accurately.&lt;/p&gt;

&lt;p&gt;In other words, the desired behavior is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pass the full download target set to Addressables for progress UI
Limit concurrent downloads with MaxConcurrentWebRequests
Immediately discard queued requests when canceled
Stop active requests as well if possible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why modify Addressables directly instead of using a custom provider?
&lt;/h2&gt;

&lt;p&gt;Addressables also allows you to create custom providers.&lt;/p&gt;

&lt;p&gt;However, based on my experience, the internal implementation of Addressables has changed significantly several times across version updates.&lt;/p&gt;

&lt;p&gt;Because of that, when modifying behavior that deeply depends on Addressables internals, it can sometimes be easier to manage the change by &lt;strong&gt;embedding the Addressables package into the project and directly modifying the relevant code&lt;/strong&gt;, rather than trying to wrap everything with a custom provider.&lt;/p&gt;

&lt;p&gt;Of course, this is more of a practical workaround than an officially recommended extension method.&lt;/p&gt;

&lt;p&gt;Directly modifying the package has several downsides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You must check diffs whenever Addressables is updated
The modified package is more likely to be outside official support
The team needs to understand and share the modification details
Unity / Addressables updates may require reapplying or rewriting the changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even so, if canceling a large download causes the app to wait for hundreds of failed network requests, directly modifying the package can be worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making Addressables editable
&lt;/h2&gt;

&lt;p&gt;Addressables installed through Package Manager is normally placed under &lt;code&gt;Library/PackageCache&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Packages in this state are not meant to be edited directly.&lt;/p&gt;

&lt;p&gt;If you want to edit Addressables, embed it into the project's &lt;code&gt;Packages&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;The official Unity workflow mainly has two options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Use the Package Manager Scripting API Client.Embed
2. Manually copy the package from Library/PackageCache to the Packages folder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Method 1: Use Client.Embed
&lt;/h3&gt;

&lt;p&gt;Unity provides the &lt;code&gt;Client.Embed&lt;/code&gt; API to embed an installed package into the project.&lt;/p&gt;

&lt;p&gt;By creating an Editor script like the following, you can embed Addressables from a menu item.&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;UnityEditor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEditor.PackageManager&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.PackageManager.Requests&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;EmbedAddressablesPackageMenu&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;EmbedRequest&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="nf"&gt;MenuItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tools/Addressables/Embed Addressables Package"&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;EmbedAddressables&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;_request&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="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;IsCompleted&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;"Addressables package embed is already running."&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;_request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.unity.addressables"&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;update&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;WaitForEmbedRequest&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;WaitForEmbedRequest&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;_request&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;_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCompleted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&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;update&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;WaitForEmbedRequest&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;Status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;StatusCode&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="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;$"Embedded package: &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;packageId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&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 embed Addressables package: &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="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;_request&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="cp"&gt;#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Client.Embed&lt;/code&gt; copies the target package into the &lt;code&gt;Packages&lt;/code&gt; folder and makes it editable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: Manually copy the package
&lt;/h3&gt;

&lt;p&gt;If you want to do it manually, first install Addressables normally from Package Manager.&lt;/p&gt;

&lt;p&gt;Then copy the Addressables folder under &lt;code&gt;Library/PackageCache&lt;/code&gt; into the &lt;code&gt;Packages&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Library/PackageCache/com.unity.addressables@8460f1c9c927
↓
Packages/com.unity.addressables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important point is that the destination folder name should usually be &lt;code&gt;com.unity.addressables&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Embedded packages under the &lt;code&gt;Packages&lt;/code&gt; folder take precedence in Unity.&lt;/p&gt;

&lt;p&gt;Because of that, even if &lt;code&gt;Packages/manifest.json&lt;/code&gt; remains unchanged, the embedded package is used first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"com.unity.addressables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.9.1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to explicitly reference it as a local package, you can also write it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"com.unity.addressables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file:com.unity.addressables"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, if you simply want it to be treated as an embedded package, placing it at &lt;code&gt;Packages/com.unity.addressables&lt;/code&gt; is usually enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modification policy
&lt;/h2&gt;

&lt;p&gt;In Addressables 2.9.1, &lt;code&gt;WebRequestQueue.cs&lt;/code&gt; has the following management structure:&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;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;s_MaxRequest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WebRequestQueueOperation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s_QueuedOperations&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;Queue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WebRequestQueueOperation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UnityWebRequestAsyncOperation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s_ActiveRequests&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UnityWebRequestAsyncOperation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Roughly speaking, they mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s_QueuedOperations = requests waiting in the queue that have not called SendWebRequest yet
s_ActiveRequests   = requests that have already called SendWebRequest and are currently running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What we want to do is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Queued requests:
  Do not start communication, and notify AssetBundleProvider that they were canceled

Active requests:
  Stop them with UnityWebRequest.Abort()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To do that, we add the following changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Add OnCancel to WebRequestQueueOperation
2. Add AbortRequest() to WebRequestQueue
3. Register cancellation handling in AssetBundleProvider through OnCancel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Modifying WebRequestQueue.cs
&lt;/h2&gt;

&lt;p&gt;First, add &lt;code&gt;OnCancel&lt;/code&gt; to &lt;code&gt;WebRequestQueueOperation&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebRequestQueueOperation&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;m_Completed&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="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Stores the async operation object returned from sending the web request.&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UnityWebRequestAsyncOperation&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Event that is invoked when the async operation is complete.&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UnityWebRequestAsyncOperation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OnComplete&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Event that is invoked when the queued request is canceled before sending.&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt; &lt;span class="n"&gt;OnCancel&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="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Indicates that the async operation is complete.&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsDone&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;return&lt;/span&gt; &lt;span class="n"&gt;m_Completed&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="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;internal&lt;/span&gt; &lt;span class="n"&gt;UnityWebRequest&lt;/span&gt; &lt;span class="n"&gt;m_WebRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// The web request.&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UnityWebRequest&lt;/span&gt; &lt;span class="n"&gt;WebRequest&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;return&lt;/span&gt; &lt;span class="n"&gt;m_WebRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;m_WebRequest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;WebRequestQueueOperation&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;m_WebRequest&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="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;void&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;UnityWebRequestAsyncOperation&lt;/span&gt; &lt;span class="n"&gt;asyncOp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;m_Completed&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;Result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncOp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;OnComplete&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;Result&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;Next, add &lt;code&gt;AbortRequest()&lt;/code&gt; to &lt;code&gt;WebRequestQueue&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebRequestQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;s_MaxRequest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WebRequestQueueOperation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s_QueuedOperations&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;Queue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WebRequestQueueOperation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UnityWebRequestAsyncOperation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s_ActiveRequests&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UnityWebRequestAsyncOperation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Cancels queued requests and aborts active requests.&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AbortRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Cancel requests that are waiting in the queue and have not started communication yet.&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;queuedOperations&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s_QueuedOperations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;s_QueuedOperations&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;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;queuedOperations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnCancel&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="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Abort requests that have already started communication.&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s_ActiveRequests&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="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;operation&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;OnWebAsyncOpComplete&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;webRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// Remove it from the active request list.&lt;/span&gt;
            &lt;span class="c1"&gt;// Since the queued operations have already been cleared,&lt;/span&gt;
            &lt;span class="c1"&gt;// this won't start the next queued request.&lt;/span&gt;
            &lt;span class="nf"&gt;OnWebAsyncOpComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;queuedOperations&lt;/code&gt; is copied with &lt;code&gt;ToArray()&lt;/code&gt; before calling &lt;code&gt;Clear()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This avoids modifying the queue while it is being enumerated, because Addressables-side completion handling may run inside &lt;code&gt;OnCancel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For active requests, &lt;code&gt;Abort()&lt;/code&gt; is called.&lt;/p&gt;

&lt;p&gt;At that point, &lt;code&gt;operation.completed -= OnWebAsyncOpComplete;&lt;/code&gt; removes the completion callback from &lt;code&gt;WebRequestQueue&lt;/code&gt;, and then &lt;code&gt;OnWebAsyncOpComplete(operation)&lt;/code&gt; is called manually.&lt;/p&gt;

&lt;p&gt;The purpose is to remove the request from &lt;code&gt;s_ActiveRequests&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;s_QueuedOperations.Clear()&lt;/code&gt; has already been called, this does not start the next queued request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modifying AssetBundleProvider.cs
&lt;/h2&gt;

&lt;p&gt;Next, register cancellation handling in &lt;code&gt;AssetBundleProvider&lt;/code&gt; through &lt;code&gt;OnCancel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In 2.9.1, there is a place where &lt;code&gt;WebRequestQueueOperation&lt;/code&gt; is received and &lt;code&gt;BeginWebRequestOperation&lt;/code&gt; is registered to &lt;code&gt;OnComplete&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Add the cancellation handling there.&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;internal&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AddBeginWebRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WebRequestQueueOperation&lt;/span&gt; &lt;span class="n"&gt;webRequestQueueOperation&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;webRequestQueueOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsDone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;BeginWebRequestOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webRequestQueueOperation&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;else&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="cp"&gt;#if ENABLE_PROFILER
&lt;/span&gt;        &lt;span class="nf"&gt;AddBundleToProfiler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Profiling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m_Source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;        &lt;span class="n"&gt;webRequestQueueOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnComplete&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;asyncOp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;BeginWebRequestOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asyncOp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Register cancellation handling for queued requests.&lt;/span&gt;
        &lt;span class="n"&gt;webRequestQueueOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnCancel&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Do not call the normal web request completion path.&lt;/span&gt;
            &lt;span class="c1"&gt;// This request was canceled before SendWebRequest was called.&lt;/span&gt;
            &lt;span class="n"&gt;webRequestQueueOperation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnComplete&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;// Notify Addressables that the provide operation has failed.&lt;/span&gt;
            &lt;span class="n"&gt;m_ProvideHandle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AssetBundleResource&lt;/span&gt;&lt;span class="p"&gt;&amp;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;false&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="cp"&gt;#if ENABLE_CACHING
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m_Options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="cp"&gt;#if ENABLE_PROFILER
&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;m_Source&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;BundleSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="c1"&gt;// Even when the operation fails, a null-like cache state can remain.&lt;/span&gt;
                    &lt;span class="c1"&gt;// If the same address is loaded or downloaded again, that cached null can be returned immediately.&lt;/span&gt;
                    &lt;span class="c1"&gt;// Clear the cached version to avoid that state.&lt;/span&gt;
                    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;locHash&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Hash128&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m_Options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hash&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;locHash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isValid&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
                        &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;Caching&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsVersionCached&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;CachedAssetBundle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m_Options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BundleName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locHash&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;Caching&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ClearCachedVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m_Options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BundleName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&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 point is that the normal &lt;code&gt;OnComplete&lt;/code&gt; path must not be called when canceled.&lt;/p&gt;

&lt;p&gt;Requests waiting in the communication queue have not called &lt;code&gt;SendWebRequest()&lt;/code&gt; yet.&lt;/p&gt;

&lt;p&gt;Therefore, instead of sending them to the normal request completion path, we notify Addressables of failure by calling &lt;code&gt;m_ProvideHandle.Complete&amp;lt;AssetBundleResource&amp;gt;(null, false, null)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;After this change, call &lt;code&gt;WebRequestQueue.AbortRequest()&lt;/code&gt; when the user wants to cancel communication.&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.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;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DownloadCancelButton&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;CancelDownload&lt;/span&gt;&lt;span class="p"&gt;()&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;AbortRequest&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 allows both of the following to be canceled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Queued requests:
  Call OnCancel and notify Addressables that the operation failed

Active requests:
  Stop communication with UnityWebRequest.Abort()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Note: This implementation is a global cancellation
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;AbortRequest()&lt;/code&gt; implementation above cancels all requests currently stored in &lt;code&gt;WebRequestQueue&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is simple when you want to stop all Addressables communication, but it is not suitable if multiple download processes are running at the same time and you only want to cancel one of them.&lt;/p&gt;

&lt;p&gt;If you want partial cancellation, extend the method so that it can filter requests by URL or internal ID.&lt;/p&gt;

&lt;p&gt;For example, you can add a &lt;code&gt;Predicate&amp;lt;UnityWebRequest&amp;gt;&lt;/code&gt; parameter like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AbortRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Predicate&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;predicate&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;queuedOperations&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s_QueuedOperations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;s_QueuedOperations&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;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;queuedOperations&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;predicate&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="nf"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WebRequest&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnCancel&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="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;s_QueuedOperations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operation&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;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s_ActiveRequests&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="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;predicate&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="p"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;webRequest&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;operation&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;OnWebAsyncOpComplete&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;webRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;OnWebAsyncOpComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operation&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;Usage example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;WebRequestQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AbortRequest&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;=&amp;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;request&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;request&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="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/addressables/"&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;Filtering by your project's CDN path or version path can reduce the risk of stopping unintended requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Note: Retry behavior for active requests
&lt;/h2&gt;

&lt;p&gt;Queued requests are explicitly completed as failed through &lt;code&gt;OnCancel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On the other hand, requests that are already active are interrupted with &lt;code&gt;UnityWebRequest.Abort()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In that case, they may still flow into the normal communication failure handling path inside Addressables.&lt;/p&gt;

&lt;p&gt;Depending on the AssetBundleProvider settings or the Addressables version, they may be retried if retry settings are enabled.&lt;/p&gt;

&lt;p&gt;If you also want active requests to be treated as a true "user cancellation", add a separate cancellation flag and branch inside &lt;code&gt;WebRequestOperationCompleted&lt;/code&gt; so that it does not enter the normal retry path.&lt;/p&gt;

&lt;p&gt;The general idea is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Set a cancellation flag when WebRequestQueue.AbortRequest() is executed
↓
Check whether cancellation is active inside AssetBundleProvider's request completion handling
↓
If canceled, do not treat it as a normal network error or retry target.
Complete it as failed instead
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This part differs depending on the Addressables version, so it is safest to check the &lt;code&gt;AssetBundleProvider.cs&lt;/code&gt; used by your project and adjust accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Note: Clearing the cache
&lt;/h2&gt;

&lt;p&gt;Even when a request is canceled or fails, a partially invalid state can remain on the cache side.&lt;/p&gt;

&lt;p&gt;If the same address is loaded or downloaded again in that state, the invalid cache state may be returned immediately.&lt;/p&gt;

&lt;p&gt;Because of that, this example clears the target Bundle cache with &lt;code&gt;Caching.ClearCachedVersion&lt;/code&gt; when canceled.&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;locHash&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Hash128&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m_Options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hash&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;locHash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isValid&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;Caching&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsVersionCached&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;CachedAssetBundle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m_Options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BundleName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locHash&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Caching&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ClearCachedVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m_Options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BundleName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locHash&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;Depending on the project, this may not be necessary.&lt;/p&gt;

&lt;p&gt;However, if the same data may be downloaded again after canceling a large download, it is worth checking how the cache behaves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;When downloading a large number of remote assets with Addressables, queued requests are stored inside Addressables even if &lt;code&gt;MaxConcurrentWebRequests&lt;/code&gt; is set.&lt;/p&gt;

&lt;p&gt;Because of that, if you submit 500 downloads at once and then cancel them, the remaining requests may still be processed one by one, causing cancellation to take a long time.&lt;/p&gt;

&lt;p&gt;If you want to pass all targets to Addressables for progress UI, app-side queue control alone may not be enough.&lt;/p&gt;

&lt;p&gt;In this approach, we directly modify &lt;code&gt;WebRequestQueue&lt;/code&gt; and &lt;code&gt;AssetBundleProvider&lt;/code&gt; in Addressables 2.9.1 and add the following behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Queued requests:
  Call OnCancel and complete them as failed before communication starts

Active requests:
  Stop communication with UnityWebRequest.Abort()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this change, both queued requests and active requests can be stopped when the user cancels.&lt;/p&gt;

&lt;p&gt;However, the internal implementation of Addressables differs depending on the version.&lt;/p&gt;

&lt;p&gt;Treat the code in this article as one example for 2.9.1, and adjust it to match the Addressables version used by your project.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Unity Manual: Embedded dependencies
&lt;a href="https://docs.unity3d.com/ja/2023.2/Manual/upm-embed.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/ja/2023.2/Manual/upm-embed.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unity Scripting API: PackageManager.Client.Embed
&lt;a href="https://docs.unity3d.com/2023.2/Documentation/ScriptReference/PackageManager.Client.Embed.html" rel="noopener noreferrer"&gt;https://docs.unity3d.com/2023.2/Documentation/ScriptReference/PackageManager.Client.Embed.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unity Manual: Local folder or tarball paths
&lt;a href="https://docs.unity.cn/Components/upm-localpath.html" rel="noopener noreferrer"&gt;https://docs.unity.cn/Components/upm-localpath.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Addressables: Asynchronous operation handles
&lt;a href="https://docs.unity.cn/Packages/com.unity.addressables@2.2/manual/AddressableAssetsAsyncOperationHandle.html" rel="noopener noreferrer"&gt;https://docs.unity.cn/Packages/com.unity.addressables@2.2/manual/AddressableAssetsAsyncOperationHandle.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>addressables</category>
      <category>assetbundle</category>
    </item>
    <item>
      <title>A Practical Guide to Unity Addressables Bundle Splitting and LZ4/LZMA Compression</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Fri, 19 Jun 2026 11:31:12 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/a-practical-guide-to-unity-addressables-bundle-splitting-and-lz4lzma-compression-49m6</link>
      <guid>https://dev.to/gamedevtoollab/a-practical-guide-to-unity-addressables-bundle-splitting-and-lz4lzma-compression-49m6</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;In the previous article, we looked at Addressables download concurrency, label design, and batch downloads using labels.&lt;/p&gt;

&lt;p&gt;One important point there was this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Labels are only the entry point for specifying what you want to download. The actual files downloaded are AssetBundles.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In other words, even if your label design looks clean, a rough Bundle layout can still cause unexpected assets to be downloaded together.&lt;/p&gt;

&lt;p&gt;This time, we will look at how to split AssetBundles and how to choose compression settings.&lt;/p&gt;

&lt;p&gt;In particular, this article covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether Bundles should be split into smaller units or grouped together&lt;/li&gt;
&lt;li&gt;How to handle many small files, such as VN / adventure game scenario text files&lt;/li&gt;
&lt;li&gt;How to split assets that will keep increasing over time, such as character 3D models&lt;/li&gt;
&lt;li&gt;How to think about &lt;code&gt;Pack Together&lt;/code&gt;, &lt;code&gt;Pack Separately&lt;/code&gt;, and &lt;code&gt;Pack Together By Label&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;How to choose between LZ4 and LZMA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not an introduction to the official features. The focus is on practical decision-making for real projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Label design and Bundle splitting must be considered together
&lt;/h2&gt;

&lt;p&gt;In Addressables, you can use Addresses and Labels to specify which assets to load or download.&lt;/p&gt;

&lt;p&gt;However, the actual files downloaded are AssetBundles.&lt;/p&gt;

&lt;p&gt;For example, suppose you define labels like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Label&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;chapter_01&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Assets used in Chapter 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chapter_02&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Assets used in Chapter 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;character_alice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Assets related to Alice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;character_bob&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Assets related to Bob&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you only look at the labels, it may seem like the assets are managed in a fine-grained way.&lt;/p&gt;

&lt;p&gt;But if the actual Bundle is packed as one large Bundle, downloading only &lt;code&gt;chapter_01&lt;/code&gt; may still download other assets included in the same Bundle.&lt;/p&gt;

&lt;p&gt;So, in Addressables, these two concepts need to be considered separately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Address / Label&lt;/td&gt;
&lt;td&gt;Specifies which assets are requested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AssetBundle&lt;/td&gt;
&lt;td&gt;The actual unit downloaded, cached, and loaded&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Label design alone does not determine the download unit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Label design and Bundle splitting must be designed together.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What does Bundle splitting affect?
&lt;/h2&gt;

&lt;p&gt;Bundle splitting is not just a way to organize things in the Editor.&lt;/p&gt;

&lt;p&gt;In practice, it affects the following:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Affected area&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Download unit&lt;/td&gt;
&lt;td&gt;Which files are downloaded together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache unit&lt;/td&gt;
&lt;td&gt;Which units are stored on the device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update unit&lt;/td&gt;
&lt;td&gt;Which Bundles become update targets during content updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load unit&lt;/td&gt;
&lt;td&gt;Which Bundles are required at load time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Number of files&lt;/td&gt;
&lt;td&gt;Affects download requests and file open operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Management cost&lt;/td&gt;
&lt;td&gt;Catalogs, dependencies, and operation rules become more complex&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Splitting Bundles into smaller units makes it easier to download only what is needed and reduce update size.&lt;/p&gt;

&lt;p&gt;On the other hand, if the number of Bundles increases too much, download requests, cached files, file open operations, catalog complexity, and dependency management cost all increase.&lt;/p&gt;

&lt;p&gt;So with Addressables, “smaller is always better” is not the right answer.&lt;/p&gt;

&lt;p&gt;The basic approach is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Group what should be grouped, and split what should be split, based on how the assets are actually used.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bundle Mode options
&lt;/h2&gt;

&lt;p&gt;In Addressables Group settings, you can choose a Bundle Mode.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bundle Mode&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Suitable cases&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Pack Together&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Packs assets in the group into Bundles together&lt;/td&gt;
&lt;td&gt;Small files, or data used together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Pack Separately&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Packs each Addressable entry in the group into a separate Bundle&lt;/td&gt;
&lt;td&gt;Characters, stages, costumes, and other assets with clear update units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Pack Together By Label&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Packs assets with the same label set together&lt;/td&gt;
&lt;td&gt;Cases where you want to manage or download by label&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One important detail is that &lt;code&gt;Pack Separately&lt;/code&gt; does not mean “one asset per Bundle.”&lt;/p&gt;

&lt;p&gt;It means &lt;strong&gt;one Bundle per Addressable entry&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If a folder is marked as an Addressable entry, the contents under that folder may be packed into one Bundle.&lt;/p&gt;

&lt;p&gt;Also, scene assets are handled as separate Bundles from normal assets. Even with &lt;code&gt;Pack Together&lt;/code&gt;, everything is not necessarily packed into a single file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid casually saying “one address, one file”
&lt;/h2&gt;

&lt;p&gt;In real projects, people sometimes describe &lt;code&gt;Pack Separately&lt;/code&gt; as “one address, one file.”&lt;/p&gt;

&lt;p&gt;That may be fine for a rough conversation, but it is better to be careful when writing design documents or articles.&lt;/p&gt;

&lt;p&gt;As an Addressables setting, the accurate term is &lt;code&gt;Pack Separately&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Pack Separately&lt;/code&gt; creates a separate Bundle for each Addressable entry in the group.&lt;/p&gt;

&lt;p&gt;So the following cases are not as simple as “one asset = one Bundle”:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A folder is registered as an Addressable entry&lt;/li&gt;
&lt;li&gt;Sprite Atlases or Sub Assets are involved&lt;/li&gt;
&lt;li&gt;Scenes and normal assets are mixed&lt;/li&gt;
&lt;li&gt;Dependencies are moved into separate Bundles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a rough explanation, “one address, one file” may be understandable.&lt;/p&gt;

&lt;p&gt;But for actual design, it is safer to think in terms of &lt;code&gt;Pack Separately&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cases where grouping Bundles is acceptable
&lt;/h2&gt;

&lt;p&gt;First, let’s look at cases where grouping assets into Bundles is reasonable.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asset characteristic&lt;/th&gt;
&lt;th&gt;How to think about it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Each file is small&lt;/td&gt;
&lt;td&gt;Creating individual Bundles may make the file count a bigger problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;There are many files&lt;/td&gt;
&lt;td&gt;Download requests and file open operations can increase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;They are used together&lt;/td&gt;
&lt;td&gt;Splitting does not change the actual load unit much&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Individual updates are rare&lt;/td&gt;
&lt;td&gt;Fine-grained update units provide little benefit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The management unit is clear&lt;/td&gt;
&lt;td&gt;Easy to group by chapter, event, language, and so on&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Typical examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VN / adventure game scenario text&lt;/li&gt;
&lt;li&gt;Dialogue text&lt;/li&gt;
&lt;li&gt;Small &lt;code&gt;.txt&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;Small configuration files&lt;/li&gt;
&lt;li&gt;Master data&lt;/li&gt;
&lt;li&gt;Localization text&lt;/li&gt;
&lt;li&gt;Lightweight ScriptableObject assets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this kind of data, creating one Bundle per file may not be the easiest approach to operate.&lt;/p&gt;

&lt;p&gt;Especially when each file is only a few KB to a few dozen KB, and there are hundreds or thousands of them, the downside of increasing the number of files can become more noticeable than the benefit of fine-grained Bundles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Many small files, such as VN scenario text
&lt;/h2&gt;

&lt;p&gt;VN / adventure game scenario text is an easy example where grouping often makes sense.&lt;/p&gt;

&lt;p&gt;For example, suppose you have a structure like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scenario/
  Chapter01/
    scene_001.txt
    scene_002.txt
    scene_003.txt
    ...
  Chapter02/
    scene_001.txt
    scene_002.txt
    scene_003.txt
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If every &lt;code&gt;.txt&lt;/code&gt; file is packed into its own Bundle, the number of files increases quickly.&lt;/p&gt;

&lt;p&gt;When the file count increases, the following costs also increase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More download requests&lt;/li&gt;
&lt;li&gt;More cached files&lt;/li&gt;
&lt;li&gt;More file open operations&lt;/li&gt;
&lt;li&gt;More file lookup and management cost&lt;/li&gt;
&lt;li&gt;Catalog and dependency checks become more tedious&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, if you need to update each scenario file independently, or if you absolutely do not want to download a scenario until the user is about to read it, splitting into smaller units may be valid.&lt;/p&gt;

&lt;p&gt;But in many cases, grouping like this is easier to operate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grouping unit&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;Chapter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scenario_chapter_01&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scenario_event_summer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;scenario_ja&lt;/code&gt;, &lt;code&gt;scenario_en&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery unit&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;scenario_initial&lt;/code&gt;, &lt;code&gt;scenario_update_001&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For scenario text, users often play by chapter or event.&lt;/p&gt;

&lt;p&gt;So grouping Bundles by those units usually makes operation easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cases where Bundles should be split
&lt;/h2&gt;

&lt;p&gt;On the other hand, there are also cases where splitting Bundles is better.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asset characteristic&lt;/th&gt;
&lt;th&gt;How to think about it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Each asset is large&lt;/td&gt;
&lt;td&gt;Downloading unnecessary data becomes costly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The addition unit is clear&lt;/td&gt;
&lt;td&gt;Easy to update by character, costume, or stage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Individual updates are frequent&lt;/td&gt;
&lt;td&gt;Update diffs can be kept smaller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The asset may not be used&lt;/td&gt;
&lt;td&gt;It can be downloaded only when needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;There is a sales or unlock unit&lt;/td&gt;
&lt;td&gt;Works well with DLC, gacha, event rewards, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Typical examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Character 3D models&lt;/li&gt;
&lt;li&gt;Character-specific Meshes&lt;/li&gt;
&lt;li&gt;Character-specific Textures&lt;/li&gt;
&lt;li&gt;Character-specific Materials&lt;/li&gt;
&lt;li&gt;Character-specific Animations&lt;/li&gt;
&lt;li&gt;Costumes&lt;/li&gt;
&lt;li&gt;Weapons&lt;/li&gt;
&lt;li&gt;Stages&lt;/li&gt;
&lt;li&gt;Large Prefabs&lt;/li&gt;
&lt;li&gt;VFX&lt;/li&gt;
&lt;li&gt;Voice assets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you group these too aggressively, operation can become painful.&lt;/p&gt;

&lt;p&gt;In particular, assets such as characters and stages often increase over time. Splitting Bundles by the unit in which they are added makes them easier to manage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assets that keep increasing, such as character 3D models
&lt;/h2&gt;

&lt;p&gt;Character 3D models are an easy example where splitting Bundles often makes sense.&lt;/p&gt;

&lt;p&gt;Suppose all characters are packed into one Bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;character_all.bundle
  Alice
  Bob
  Carol
  Dave
  Eve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this layout, adding just one new character can make the entire &lt;code&gt;character_all.bundle&lt;/code&gt; an update target.&lt;/p&gt;

&lt;p&gt;Even if a user only needs Alice, they may still download Bob and the other characters because they are in the same Bundle.&lt;/p&gt;

&lt;p&gt;On the other hand, if you split Bundles by character, the update and download units become clearer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;character_alice.bundle
character_bob.bundle
character_carol.bundle
character_dave.bundle
character_eve.bundle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this layout, if only Alice is needed, only Alice’s Bundle can be downloaded.&lt;/p&gt;

&lt;p&gt;When adding a new character, you can add a new Bundle and avoid updating existing character Bundles as much as possible.&lt;/p&gt;

&lt;p&gt;A character Bundle may contain assets like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;character_alice.bundle
  Alice.prefab
  Alice.mesh
  Alice.material
  Alice_texture_base
  Alice_texture_normal
  Alice_animation_idle
  Alice_animation_run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of layout is practical because &lt;strong&gt;larger assets that are used together are grouped by their addition and update unit&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;Pack Together By Label&lt;/code&gt; is useful, but depends on label design
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Pack Together By Label&lt;/code&gt; packs assets with the same label set into Bundles.&lt;/p&gt;

&lt;p&gt;If your label design is clean, this can make it easier to create Bundles by label.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asset&lt;/th&gt;
&lt;th&gt;Labels&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Alice-related assets&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;character_alice&lt;/code&gt;, &lt;code&gt;initial_download&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bob-related assets&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;character_bob&lt;/code&gt;, &lt;code&gt;event_001&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chapter 1 scenario&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;scenario_chapter_01&lt;/code&gt;, &lt;code&gt;initial_download&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chapter 2 scenario&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;scenario_chapter_02&lt;/code&gt;, &lt;code&gt;update_001&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;However, if labels are messy, relying on &lt;code&gt;Pack Together By Label&lt;/code&gt; can easily produce an unintended Bundle layout.&lt;/p&gt;

&lt;p&gt;For example, if management labels, download labels, event labels, and search labels are mixed together, the number of label combinations increases and the Bundle layout becomes hard to read.&lt;/p&gt;

&lt;p&gt;When using &lt;code&gt;Pack Together By Label&lt;/code&gt;, it is safer to separate label responsibilities:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Label type&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Download labels&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;initial_download&lt;/code&gt;, &lt;code&gt;chapter_01&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;These may affect Bundle splitting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Classification labels&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;character&lt;/code&gt;, &lt;code&gt;scenario&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Decide whether they should affect Bundle splitting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Management labels&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;deprecated&lt;/code&gt;, &lt;code&gt;debug&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Avoid mixing them with runtime labels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event labels&lt;/td&gt;
&lt;td&gt;&lt;code&gt;event_001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Also consider what happens after the event ends&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Increasing the number of labels is not bad by itself.&lt;/p&gt;

&lt;p&gt;But when using &lt;code&gt;Pack Together By Label&lt;/code&gt;, the label set directly affects the Bundle layout, so you need operation rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to decide whether to group or split
&lt;/h2&gt;

&lt;p&gt;As a rough guide:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asset characteristic&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Many small files&lt;/td&gt;
&lt;td&gt;Group them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Used together by chapter or event&lt;/td&gt;
&lt;td&gt;Group them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Little need for individual updates&lt;/td&gt;
&lt;td&gt;Group them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Too many files are being created&lt;/td&gt;
&lt;td&gt;Group them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Each asset is large&lt;/td&gt;
&lt;td&gt;Split them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addition unit is clear&lt;/td&gt;
&lt;td&gt;Split them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Added by character, costume, or stage&lt;/td&gt;
&lt;td&gt;Split them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You do not want to download unused assets&lt;/td&gt;
&lt;td&gt;Split them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want to keep update diffs small&lt;/td&gt;
&lt;td&gt;Split them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In more practical terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Group many small files&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Split large assets with clear addition units&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Group assets that are used together&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Split assets that may not be used&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Split assets with different update units&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the basic rules for Bundle splitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Difference between LZ4 and LZMA
&lt;/h2&gt;

&lt;p&gt;Next, let’s look at compression settings.&lt;/p&gt;

&lt;p&gt;In Addressables Group settings, you can choose the compression format for Bundles.&lt;/p&gt;

&lt;p&gt;The common choices are LZ4 and LZMA.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Compression&lt;/th&gt;
&lt;th&gt;Characteristics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LZMA&lt;/td&gt;
&lt;td&gt;High compression ratio, useful for reducing download size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LZ4&lt;/td&gt;
&lt;td&gt;Chunk-based and easier to handle at load time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uncompressed&lt;/td&gt;
&lt;td&gt;No decompression cost, but file size tends to be larger&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;LZMA treats the entire Bundle as one compressed stream.&lt;/p&gt;

&lt;p&gt;This usually gives a higher compression ratio, but the decompression cost during loading can become larger.&lt;/p&gt;

&lt;p&gt;LZ4 compresses data in chunks.&lt;/p&gt;

&lt;p&gt;Its compression ratio is usually lower than LZMA, but it is easier to read only the needed parts, and it tends to be lighter to handle at load time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cases where LZMA is easy to recommend
&lt;/h2&gt;

&lt;p&gt;LZMA is useful when you want to reduce download size.&lt;/p&gt;

&lt;p&gt;However, in real projects, it is safer to decide based on &lt;strong&gt;what is inside the Bundle&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;As a rule of thumb:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If 1 Bundle is close to 1 asset, LZMA is easy to recommend.
If 1 Bundle contains multiple assets, LZ4 should be the default.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, “1 Bundle = 1 asset” means cases like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One large binary file&lt;/li&gt;
&lt;li&gt;One large TextAsset&lt;/li&gt;
&lt;li&gt;One video file&lt;/li&gt;
&lt;li&gt;One audio file&lt;/li&gt;
&lt;li&gt;One Prefab packed into its own Bundle&lt;/li&gt;
&lt;li&gt;One Texture packed into its own Bundle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These Bundles are likely to be loaded almost entirely when used.&lt;/p&gt;

&lt;p&gt;That makes it easier to benefit from LZMA’s high compression ratio.&lt;/p&gt;

&lt;p&gt;On the other hand, be careful with a Bundle like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;character_alice.bundle
  Alice.prefab
  Alice.mesh
  Alice.material
  Alice_texture_base
  Alice_texture_normal
  Alice_animation_idle
  Alice_animation_run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a “one character, one Bundle” layout, but the Bundle still contains multiple assets.&lt;/p&gt;

&lt;p&gt;Even if it looks like one unit called Alice at the application level, the Bundle itself is a collection of multiple assets.&lt;/p&gt;

&lt;p&gt;So it is safer not to casually decide “one character Bundle means LZMA.”&lt;/p&gt;

&lt;p&gt;LZMA is easy to recommend only when the layout is close to &lt;strong&gt;1 Bundle = 1 asset&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use LZ4 as the default for Bundles containing multiple assets
&lt;/h2&gt;

&lt;p&gt;LZMA treats the entire Bundle as one compressed stream.&lt;/p&gt;

&lt;p&gt;So even if you only want to read part of the Bundle, the decompression cost of the whole Bundle can become an issue at load time.&lt;/p&gt;

&lt;p&gt;When multiple independent assets are grouped into one Bundle, LZ4 is usually easier to operate.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bundle layout where LZ4 should be the default&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bundle containing multiple Addressable entries&lt;/td&gt;
&lt;td&gt;You may only read part of it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle containing many small files&lt;/td&gt;
&lt;td&gt;The purpose is reducing file count, so partial access matters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chapter-level scenario Bundle&lt;/td&gt;
&lt;td&gt;You may not read every file in the chapter at once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Master data Bundle&lt;/td&gt;
&lt;td&gt;Only some data may be referenced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Localization Bundle&lt;/td&gt;
&lt;td&gt;Data may be read by language or screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Character-related asset Bundle&lt;/td&gt;
&lt;td&gt;Often contains Prefab, Mesh, Texture, Animation, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle where stable load time and memory behavior matter&lt;/td&gt;
&lt;td&gt;Avoiding full-stream decompression is safer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important point is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“One character, one Bundle” and “one Bundle, one asset” are different things.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even if a character has its own Bundle, if that Bundle contains a Prefab, Meshes, Textures, Materials, and Animations, it is still a multi-asset Bundle.&lt;/p&gt;

&lt;p&gt;In that case, using LZ4 as the default usually makes load-time behavior more stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cases where LZ4 should be the default
&lt;/h2&gt;

&lt;p&gt;LZ4 is suitable when you care about load-time handling.&lt;/p&gt;

&lt;p&gt;In Addressables, it is common to group multiple assets into one Bundle. In practice, &lt;strong&gt;LZ4 is usually the default choice for multi-asset Bundles&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;Bundle layout&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bundle containing many small files&lt;/td&gt;
&lt;td&gt;You may only read part of it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chapter-level scenario Bundle&lt;/td&gt;
&lt;td&gt;Files are often read in sequence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Master data Bundle&lt;/td&gt;
&lt;td&gt;Only some data may be referenced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Localization Bundle&lt;/td&gt;
&lt;td&gt;Data may be read by language or screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle containing multiple Addressable entries&lt;/td&gt;
&lt;td&gt;LZMA is more affected by whole-Bundle decompression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Character-related asset Bundle&lt;/td&gt;
&lt;td&gt;Often contains multiple assets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle where stable load time matters&lt;/td&gt;
&lt;td&gt;Chunk-based compression is easier to operate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For Bundles that group multiple files, such as VN scenario text, LZ4 is often easier to handle.&lt;/p&gt;

&lt;p&gt;Group the Bundle to reduce the number of files.&lt;/p&gt;

&lt;p&gt;But use LZ4 because you may only read part of the Bundle.&lt;/p&gt;

&lt;p&gt;This combination is very practical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical recommended settings
&lt;/h2&gt;

&lt;p&gt;As a starting point, the following rules are easy to work with:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asset type&lt;/th&gt;
&lt;th&gt;Bundle split&lt;/th&gt;
&lt;th&gt;Compression&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One large TextAsset&lt;/td&gt;
&lt;td&gt;1 asset per Bundle&lt;/td&gt;
&lt;td&gt;LZMA recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One large binary file&lt;/td&gt;
&lt;td&gt;1 asset per Bundle&lt;/td&gt;
&lt;td&gt;LZMA recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One video file&lt;/td&gt;
&lt;td&gt;1 asset per Bundle&lt;/td&gt;
&lt;td&gt;LZMA recommended, but also consider compression of the video format itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One audio file&lt;/td&gt;
&lt;td&gt;1 asset per Bundle&lt;/td&gt;
&lt;td&gt;LZMA recommended, but also consider compression of the audio format itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One Texture&lt;/td&gt;
&lt;td&gt;1 asset per Bundle&lt;/td&gt;
&lt;td&gt;LZMA recommended, but judge based on the size after texture compression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VN scenario text&lt;/td&gt;
&lt;td&gt;Group by chapter or event&lt;/td&gt;
&lt;td&gt;LZ4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dialogue text&lt;/td&gt;
&lt;td&gt;Group by chapter, event, or language&lt;/td&gt;
&lt;td&gt;LZ4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Master data&lt;/td&gt;
&lt;td&gt;Group by type or update unit&lt;/td&gt;
&lt;td&gt;LZ4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Localization&lt;/td&gt;
&lt;td&gt;Group by language&lt;/td&gt;
&lt;td&gt;LZ4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Character 3D models&lt;/td&gt;
&lt;td&gt;Consider one Bundle per character&lt;/td&gt;
&lt;td&gt;LZ4 if the Bundle contains multiple assets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Costumes&lt;/td&gt;
&lt;td&gt;Consider one Bundle per costume&lt;/td&gt;
&lt;td&gt;LZ4 if the Bundle contains multiple assets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stages&lt;/td&gt;
&lt;td&gt;Consider one Bundle per stage&lt;/td&gt;
&lt;td&gt;LZ4 if the Bundle contains multiple assets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large VFX&lt;/td&gt;
&lt;td&gt;Split by usage unit&lt;/td&gt;
&lt;td&gt;LZ4 if the Bundle contains multiple assets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice assets&lt;/td&gt;
&lt;td&gt;Split by character, chapter, or event&lt;/td&gt;
&lt;td&gt;LZ4 if multiple files are grouped together&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key point is to think about Bundle granularity and compression format separately.&lt;/p&gt;

&lt;p&gt;Splitting Bundles, such as one Bundle per character, is useful for reducing download and update units.&lt;/p&gt;

&lt;p&gt;However, if that Bundle contains multiple assets, LZ4 should be the default compression choice.&lt;/p&gt;

&lt;p&gt;Conversely, if a Bundle contains only one asset, LZMA is easy to recommend.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 Bundle is close to 1 asset&lt;/td&gt;
&lt;td&gt;LZMA recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The whole Bundle is always read&lt;/td&gt;
&lt;td&gt;LZMA is easier to recommend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You strongly want to reduce initial download size&lt;/td&gt;
&lt;td&gt;LZMA is easier to recommend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple Addressable entries are included&lt;/td&gt;
&lt;td&gt;LZ4 by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You may only read part of the Bundle&lt;/td&gt;
&lt;td&gt;LZ4 by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Many small files are grouped together&lt;/td&gt;
&lt;td&gt;LZ4 by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want stable load time&lt;/td&gt;
&lt;td&gt;LZ4 by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web delivery is involved&lt;/td&gt;
&lt;td&gt;Start with LZ4 in mind&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Of course, the final decision should be based on real device measurements.&lt;/p&gt;

&lt;p&gt;Even with “1 asset per Bundle,” the perceived behavior changes depending on asset type, size, device performance, and load timing.&lt;/p&gt;

&lt;p&gt;Measure download size, load time, and memory usage on real devices before making the final decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notes on Bundle splitting
&lt;/h2&gt;

&lt;p&gt;There are a few additional things to keep in mind when splitting Bundles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Duplicate dependencies
&lt;/h3&gt;

&lt;p&gt;When Bundles are split into smaller units, shared dependencies become important.&lt;/p&gt;

&lt;p&gt;For example, if multiple characters reference the same Material or Texture, the dependency layout can become complicated depending on how you split the Bundles.&lt;/p&gt;

&lt;p&gt;You need operation rules for whether shared assets should be moved into a shared Bundle or kept with each character.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not look only at update diffs
&lt;/h3&gt;

&lt;p&gt;Splitting Bundles into smaller units makes update diffs easier to reduce.&lt;/p&gt;

&lt;p&gt;But the trade-off is an increased number of Bundles.&lt;/p&gt;

&lt;p&gt;If you only look at update size, you may want to split everything. But download requests, cache count, and load-time management cost should be considered together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Think about initial downloads and additional downloads separately
&lt;/h3&gt;

&lt;p&gt;Assets needed for the initial download and assets downloaded later should not be judged by the same rule.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;How to think about it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initial download&lt;/td&gt;
&lt;td&gt;Group assets needed immediately after launch and reduce the number of downloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Additional download&lt;/td&gt;
&lt;td&gt;Download only what is needed by character, stage, or event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optional download&lt;/td&gt;
&lt;td&gt;Download voices, high-resolution textures, etc. only when needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For initial downloads, grouping more aggressively can be easier.&lt;/p&gt;

&lt;p&gt;For additional downloads, making the unit clear is usually easier to operate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not mix the responsibilities of labels and Bundle Mode too much
&lt;/h3&gt;

&lt;p&gt;Labels are useful, but trying to solve everything with labels can break the design.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Labels for specifying download targets&lt;/li&gt;
&lt;li&gt;Labels for search or classification&lt;/li&gt;
&lt;li&gt;Labels for management&lt;/li&gt;
&lt;li&gt;Labels for events&lt;/li&gt;
&lt;li&gt;Labels for debugging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all of these are mixed together and you use &lt;code&gt;Pack Together By Label&lt;/code&gt;, the Bundle layout becomes difficult to understand.&lt;/p&gt;

&lt;p&gt;When adding labels, consider whether each label is allowed to affect Bundle splitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical checklist
&lt;/h2&gt;

&lt;p&gt;Finally, here is a checklist for deciding Bundle layout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Group or split
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Are these assets used together?&lt;/li&gt;
&lt;li&gt;[ ] Do you want to download them individually?&lt;/li&gt;
&lt;li&gt;[ ] Do you want to update them individually?&lt;/li&gt;
&lt;li&gt;[ ] Is each file large?&lt;/li&gt;
&lt;li&gt;[ ] Is the number of files getting too high?&lt;/li&gt;
&lt;li&gt;[ ] Is there a clear unit such as character, chapter, stage, or event?&lt;/li&gt;
&lt;li&gt;[ ] Is this part of the initial download or an additional download?&lt;/li&gt;
&lt;li&gt;[ ] Are unnecessary assets being downloaded?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Compression settings
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Is download size the priority?&lt;/li&gt;
&lt;li&gt;[ ] Is load time the priority?&lt;/li&gt;
&lt;li&gt;[ ] Is the layout close to 1 Bundle = 1 asset?&lt;/li&gt;
&lt;li&gt;[ ] Does the Bundle contain multiple assets?&lt;/li&gt;
&lt;li&gt;[ ] Is there a chance that only part of the Bundle will be read?&lt;/li&gt;
&lt;li&gt;[ ] Did you measure download size on a real device?&lt;/li&gt;
&lt;li&gt;[ ] Did you measure load time on a real device?&lt;/li&gt;
&lt;li&gt;[ ] Did you measure memory usage on a real device?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Operation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Does the label design match the Bundle layout?&lt;/li&gt;
&lt;li&gt;[ ] Are you relying too much on &lt;code&gt;Pack Together By Label&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;[ ] Have you decided how to handle shared dependencies?&lt;/li&gt;
&lt;li&gt;[ ] Are you balancing update size and file count?&lt;/li&gt;
&lt;li&gt;[ ] Will the design still work after more assets are added post-release?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;With Addressables Bundle splitting, neither “split everything” nor “group everything” is always correct.&lt;/p&gt;

&lt;p&gt;The important thing is to decide based on how the assets are used, update units, download units, file count, and load-time cost.&lt;/p&gt;

&lt;p&gt;For data such as VN scenario text, where each file is small but there are many files, grouping by chapter or event can be easier to operate.&lt;/p&gt;

&lt;p&gt;This is because increasing the number of files also increases download requests, cached files, file open operations, file lookup cost, and management cost.&lt;/p&gt;

&lt;p&gt;On the other hand, for assets such as character 3D models, where each asset is large and new assets are likely to be added over time, splitting by character can be easier to manage.&lt;/p&gt;

&lt;p&gt;For compression, the rule of thumb is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If 1 Bundle is close to 1 asset, LZMA is recommended. If the Bundle contains multiple assets, use LZ4 by default.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LZMA is useful when compression ratio is the priority, but because whole-Bundle decompression can become an issue, it is safer to avoid it when multiple independent load targets are grouped into one Bundle.&lt;/p&gt;

&lt;p&gt;On the other hand, if the Bundle effectively contains one asset, such as one large TextAsset, one binary file, or one Texture, LZMA is easy to recommend.&lt;/p&gt;

&lt;p&gt;In the end, always measure download size, load time, and memory usage on real devices before making the final decision.&lt;/p&gt;

&lt;p&gt;Addressables can look complicated if you only look at the settings.&lt;/p&gt;

&lt;p&gt;But in practice, the basic idea is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Group small files. Split large assets with clear addition units. For compression, use LZMA when 1 Bundle is close to 1 asset, and use LZ4 by default for multi-asset Bundles.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Starting from this policy makes it easier to build a Bundle layout that does not break down over time.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>addressables</category>
      <category>assetbundle</category>
    </item>
    <item>
      <title>Practical Tips for Unity Addressables: Concurrent Downloads and Label Design</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Fri, 19 Jun 2026 11:29:54 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/practical-tips-for-unity-addressables-concurrent-downloads-and-label-design-1dho</link>
      <guid>https://dev.to/gamedevtoollab/practical-tips-for-unity-addressables-concurrent-downloads-and-label-design-1dho</guid>
      <description>&lt;h1&gt;
  
  
  Overview
&lt;/h1&gt;

&lt;p&gt;Unity Addressables makes it much easier to handle remote asset downloads and cache management.&lt;/p&gt;

&lt;p&gt;However, when you use it in a real project, you may run into issues like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calling &lt;code&gt;DownloadDependenciesAsync&lt;/code&gt; does not make downloads as fast as expected&lt;/li&gt;
&lt;li&gt;Downloading by label pulls in assets you did not expect&lt;/li&gt;
&lt;li&gt;Manually managing Address and Label settings becomes painful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article focuses on the points that are easy to overlook when designing downloads with Addressables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configuring the number of concurrent downloads&lt;/li&gt;
&lt;li&gt;How to think about concurrent downloads on iOS and Android&lt;/li&gt;
&lt;li&gt;Why you should start downloading larger files first&lt;/li&gt;
&lt;li&gt;Bulk downloads using labels&lt;/li&gt;
&lt;li&gt;The difference between labels and AssetBundle download units&lt;/li&gt;
&lt;li&gt;Automating management with Smart Addresser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bundle splitting and LZ4/LZMA compression will be covered in a separate article, so this article only touches on them where necessary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Addressables download speed is not determined by a single API call
&lt;/h2&gt;

&lt;p&gt;When pre-downloading assets with Addressables, the API you will often use is:&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;Addressables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DownloadDependenciesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful because it allows you to download the dependencies associated with a label or address ahead of time.&lt;/p&gt;

&lt;p&gt;That makes the user experience more stable than suddenly downloading assets right before a loading sequence.&lt;/p&gt;

&lt;p&gt;However, download speed is not determined just by calling this API.&lt;/p&gt;

&lt;p&gt;In practice, it depends on factors such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The number of concurrent downloads&lt;/li&gt;
&lt;li&gt;CDN or server response speed&lt;/li&gt;
&lt;li&gt;The user's network condition&lt;/li&gt;
&lt;li&gt;AssetBundle size&lt;/li&gt;
&lt;li&gt;The number of AssetBundles&lt;/li&gt;
&lt;li&gt;Device performance&lt;/li&gt;
&lt;li&gt;Decompression and loading cost after download&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Especially in mobile games, &lt;strong&gt;the number of concurrent downloads&lt;/strong&gt; and &lt;strong&gt;the order in which downloads are started&lt;/strong&gt; are very important.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configure the number of concurrent downloads properly
&lt;/h2&gt;

&lt;p&gt;Addressables allows you to control how many web requests run at the same time.&lt;/p&gt;

&lt;p&gt;At runtime, you can configure it with the following API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="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="n"&gt;maxRequests&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 number of concurrent downloads is too low, you may not fully utilize the available network or CDN performance.&lt;/p&gt;

&lt;p&gt;On the other hand, if it is too high, you may run into problems such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Increased load on low-end devices&lt;/li&gt;
&lt;li&gt;Lower frame rate during downloads&lt;/li&gt;
&lt;li&gt;More heat generation&lt;/li&gt;
&lt;li&gt;Bottlenecks during decompression or loading after downloads&lt;/li&gt;
&lt;li&gt;Instability depending on the network condition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, more concurrent downloads do not always mean faster downloads.&lt;/p&gt;

&lt;p&gt;You need to tune this based on your target devices, Bundle size, network environment, and loading screen design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Use 10 on iOS, and switch between 3 and 10 on Android based on device performance
&lt;/h2&gt;

&lt;p&gt;In my projects, I use the following policy based on real-device testing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Concurrent downloads&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-end Android devices&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low- to mid-range Android devices&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important point is that this is not an official fixed recommendation from Unity.&lt;/p&gt;

&lt;p&gt;It is a practical policy adopted after testing within a specific project.&lt;/p&gt;

&lt;p&gt;:::message&lt;br&gt;
In this article, I use 10 concurrent downloads on iOS and either 3 or 10 on Android depending on device performance, as a practical policy based on real-device testing.&lt;/p&gt;

&lt;p&gt;The optimal value depends on your CDN, network condition, Bundle size, and device performance, so make sure to measure it on actual devices.&lt;br&gt;
:::&lt;/p&gt;

&lt;p&gt;iOS devices tend to have less variation in performance, so it is easier to use a relatively aggressive setting.&lt;/p&gt;

&lt;p&gt;Android devices vary much more, so it is safer to switch the value based on device performance instead of using 10 for every device.&lt;/p&gt;


&lt;h2&gt;
  
  
  Configure concurrent downloads at runtime
&lt;/h2&gt;

&lt;p&gt;If you want to change the number of concurrent downloads per device, call &lt;code&gt;WebRequestQueue.SetMaxConcurrentRequests()&lt;/code&gt; when the app starts.&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.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;AddressablesDownloadTuning&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;Apply&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxRequests&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetMaxConcurrentRequests&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="n"&gt;maxRequests&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;$"Addressables max concurrent requests: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;maxRequests&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;GetMaxConcurrentRequests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="cp"&gt;#if UNITY_IOS
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#elif UNITY_ANDROID
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;GetAndroidDeviceScore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#else
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cp"&gt;#if UNITY_ANDROID
&lt;/span&gt;    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;GetAndroidDeviceScore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SystemInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemMemorySize&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;6000&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="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SystemInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemMemorySize&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;4000&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="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SystemInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processorCount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;8&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="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SystemInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;graphicsMemorySize&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;2048&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="m"&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="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="cp"&gt;#endif
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call it during app initialization.&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;class&lt;/span&gt; &lt;span class="nc"&gt;AppInitializer&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;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;AddressablesDownloadTuning&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is to configure this before Addressables downloads begin.&lt;/p&gt;

&lt;p&gt;It is easier to manage if you call it at a fixed timing, such as during initial app setup or around Addressables initialization.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adjust Android scoring for your project
&lt;/h2&gt;

&lt;p&gt;In the sample above, the Android device score is calculated using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;System memory&lt;/li&gt;
&lt;li&gt;CPU core count&lt;/li&gt;
&lt;li&gt;GPU memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, this scoring method is only an example.&lt;/p&gt;

&lt;p&gt;In practice, the best criteria will differ depending on your project's target devices, Bundle sizes, download-time load, and loading screen design.&lt;/p&gt;

&lt;p&gt;For example, you may want to tune it while checking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimum target device specs&lt;/li&gt;
&lt;li&gt;FPS during downloads&lt;/li&gt;
&lt;li&gt;Memory usage during downloads&lt;/li&gt;
&lt;li&gt;Decompression and loading time after downloads&lt;/li&gt;
&lt;li&gt;Heat generation&lt;/li&gt;
&lt;li&gt;Network conditions&lt;/li&gt;
&lt;li&gt;Whether other loading tasks also run during downloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Android devices vary widely, so you cannot simply say, "This device has a lot of memory, so 10 is fine."&lt;/p&gt;

&lt;p&gt;Even if the memory and CPU specs look good, storage performance or network conditions can still make download behavior unstable.&lt;/p&gt;

&lt;p&gt;For that reason, device scoring should not be treated as a fixed answer.&lt;/p&gt;

&lt;p&gt;It should be adjusted per project based on real-device testing.&lt;/p&gt;

&lt;p&gt;Personally, I first decide a safe value for low-end devices, then increase the number of concurrent downloads only for higher-end devices.&lt;/p&gt;




&lt;h2&gt;
  
  
  Start downloading larger files first when downloading multiple files
&lt;/h2&gt;

&lt;p&gt;When configuring concurrent downloads, there is one more thing to consider: the order in which downloads are started.&lt;/p&gt;

&lt;p&gt;If you manage your own queue of multiple Bundles or labels, it is generally better to &lt;strong&gt;start with the largest files first&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The reason is that you want to avoid leaving one huge file until the very end.&lt;/p&gt;

&lt;p&gt;For example, suppose the maximum number of concurrent downloads is 3 and you have the following download targets.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Size&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;300MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;20MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;10MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;8MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;5MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you start from the smallest files, the download may look smooth at first.&lt;/p&gt;

&lt;p&gt;However, if the 300MB file is left until the end, the progress can appear to stall near the end of the loading screen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Starting from smaller files

[ 5MB ] [ 8MB ] [ 10MB ]
[ 20MB ]
[ 300MB ] &amp;lt;- Only the largest file remains at the end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the other hand, if you start downloading the larger file first, the 300MB file can progress in the background while smaller files are processed in the remaining request slots.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Starting from larger files

[ 300MB ] [ 20MB ] [ 10MB ]
[ 8MB ] [ 5MB ] &amp;lt;- Smaller files are processed while the larger one is already running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even with the same number of concurrent downloads, the perceived waiting time can change depending on the order.&lt;/p&gt;

&lt;p&gt;This is especially useful when you download multiple additional assets together on a loading screen.&lt;/p&gt;

&lt;p&gt;In that case, it is usually better to enqueue larger Bundles first.&lt;/p&gt;

&lt;p&gt;However, when you call &lt;code&gt;Addressables.DownloadDependenciesAsync(label)&lt;/code&gt; for a single label, it is difficult to precisely control which Bundle is downloaded first internally.&lt;/p&gt;

&lt;p&gt;If you want explicit control over the download order, split your download targets by label or key, get their sizes in advance, and then start downloads in descending order of size.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections.Generic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;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;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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddressablesPreloadQueue&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;preloadLabels&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;"scenario_chapter_01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"voice_jp_chapter_01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"character_hero_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;PreloadBySize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;preloadLabels&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;PreloadBySize&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;labels&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="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DownloadTarget&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;labels&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;sizeHandle&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;GetDownloadSizeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&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;sizeHandle&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;sizeHandle&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;&amp;amp;&amp;amp;&lt;/span&gt;
                &lt;span class="n"&gt;sizeHandle&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;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DownloadTarget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sizeHandle&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="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;sizeHandle&lt;/span&gt;&lt;span class="p"&gt;);&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;Sort&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;b&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;b&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="nf"&gt;CompareTo&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;Size&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;handles&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AsyncOperationHandle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;targets&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;$"Download start: &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;Label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, size: &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;Size&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes"&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;Addressables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DownloadDependenciesAsync&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;Label&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;handles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handles&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="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;IsDone&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;handles&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="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;"Download failed."&lt;/span&gt;&lt;span class="p"&gt;);&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;readonly&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;DownloadTarget&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;Size&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;DownloadTarget&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;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Label&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;label&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;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important point is that &lt;code&gt;WebRequestQueue.SetMaxConcurrentRequests()&lt;/code&gt; controls the number of concurrent downloads, while your own queue controls the order in which downloads are started.&lt;/p&gt;

&lt;p&gt;For example, if the concurrent request limit is 3, even if you start downloads in descending order of size, only up to 3 web requests will run at the same time.&lt;/p&gt;

&lt;p&gt;In short, Addressables bulk downloads become more stable when you think about these three points together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Match the number of concurrent downloads to device performance&lt;/li&gt;
&lt;li&gt;Enqueue download targets from largest to smallest&lt;/li&gt;
&lt;li&gt;Design labels and Bundle splitting together&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Bulk downloads using labels
&lt;/h2&gt;

&lt;p&gt;Addressables allows you to use labels to handle multiple assets as a group.&lt;/p&gt;

&lt;p&gt;For example, if you want to download all assets with the &lt;code&gt;preload&lt;/code&gt; label, you can write:&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;Addressables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DownloadDependenciesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, in production, it is usually better to check the download size before starting the download.&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;Addressables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDownloadSizeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"preload"&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 assets are already cached, the download size will be &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This allows you to implement behavior such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check whether a download is required&lt;/li&gt;
&lt;li&gt;Show the download size to the user&lt;/li&gt;
&lt;li&gt;Show a Wi-Fi recommendation&lt;/li&gt;
&lt;li&gt;Skip the process if the assets are already downloaded&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Sample code for DownloadDependenciesAsync
&lt;/h2&gt;

&lt;p&gt;The following sample checks the download size for a label, then downloads it if necessary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddressablesPreloader&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;preloadLabel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"preload"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Preload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;preloadLabel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;Preload&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;label&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;sizeHandle&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;GetDownloadSizeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&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;sizeHandle&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;sizeHandle&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="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 get download size: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&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;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;sizeHandle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;downloadSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sizeHandle&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;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;sizeHandle&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;downloadSize&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;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;$"Already downloaded: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&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;yield&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;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;$"Download size: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;downloadSize&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes"&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;downloadHandle&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;DownloadDependenciesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&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;while&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;downloadHandle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsDone&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;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;downloadHandle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDownloadStatus&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;$"Download progress: &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;Percent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;P0&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;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;downloadHandle&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="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;$"Download completed: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&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;$"Download failed: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="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;downloadHandle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this sample, &lt;code&gt;DownloadDependenciesAsync(label, false)&lt;/code&gt; is used, and &lt;code&gt;Addressables.Release(downloadHandle)&lt;/code&gt; is called manually after completion.&lt;/p&gt;

&lt;p&gt;If you want to display progress or check the result, it is easier to manage the lifetime of the handle yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to design labels
&lt;/h2&gt;

&lt;p&gt;A label should be designed as a key that represents which group of assets you want to handle together at runtime.&lt;/p&gt;

&lt;p&gt;For example, you might use labels like these:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Label&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;preload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimum assets required on first launch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scenario_chapter_01&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scenario text, backgrounds, and character sprites used in chapter 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;event_2026_summer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Assets for a limited-time summer event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;character_hero_001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Assets for a specific character&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;voice_jp_chapter_01&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Japanese voice assets for chapter 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important point is to design labels based on the actual loading flow.&lt;/p&gt;

&lt;p&gt;For example, in an ADV or visual-novel-style game, you might use groups such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Common UI required on first launch&lt;/li&gt;
&lt;li&gt;Title screen&lt;/li&gt;
&lt;li&gt;Tutorial&lt;/li&gt;
&lt;li&gt;Scenario assets per chapter&lt;/li&gt;
&lt;li&gt;Backgrounds per chapter&lt;/li&gt;
&lt;li&gt;Character sprites per chapter&lt;/li&gt;
&lt;li&gt;Voice assets per language&lt;/li&gt;
&lt;li&gt;Limited-time events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a game where characters are added over time, labels like these may also work well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;character_hero_001&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;character_hero_002&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;skin_hero_001_summer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;voice_hero_001_jp&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Label design becomes much easier if you work backward from when the user needs each group of assets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Labels are assigned to assets, but downloads happen per Bundle
&lt;/h2&gt;

&lt;p&gt;This is an easy point to misunderstand.&lt;/p&gt;

&lt;p&gt;Addressables labels are assigned to Addressable assets.&lt;/p&gt;

&lt;p&gt;However, what actually gets downloaded is the AssetBundle.&lt;/p&gt;

&lt;p&gt;:::message alert&lt;br&gt;
A label is a key used to specify which group of assets should be downloaded.&lt;/p&gt;

&lt;p&gt;However, the actual download unit is the AssetBundle.&lt;/p&gt;

&lt;p&gt;So even if you assign a label to an asset, it does not necessarily mean only that specific asset will be downloaded in isolation.&lt;br&gt;
:::&lt;/p&gt;

&lt;p&gt;For example, suppose you have the following setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;A.prefab&lt;/code&gt; has the &lt;code&gt;preload&lt;/code&gt; label&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;B.prefab&lt;/code&gt; does not have the &lt;code&gt;preload&lt;/code&gt; label&lt;/li&gt;
&lt;li&gt;But &lt;code&gt;A.prefab&lt;/code&gt; and &lt;code&gt;B.prefab&lt;/code&gt; are included in the same AssetBundle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this case, even if you download using the &lt;code&gt;preload&lt;/code&gt; label, the AssetBundle that contains &lt;code&gt;A.prefab&lt;/code&gt; will be downloaded.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;B.prefab&lt;/code&gt; is also included in that same AssetBundle, &lt;code&gt;B.prefab&lt;/code&gt; will effectively be downloaded as well.&lt;/p&gt;

&lt;p&gt;In other words, labels are the entry point for selecting download targets, but the actual file unit that gets downloaded is the AssetBundle.&lt;/p&gt;


&lt;h2&gt;
  
  
  Design labels and Bundle splitting together
&lt;/h2&gt;

&lt;p&gt;Even if your labels are finely designed, the download unit remains large if your Bundles are grouped too broadly.&lt;/p&gt;

&lt;p&gt;On the other hand, if you split Bundles too aggressively, the number of files increases.&lt;/p&gt;

&lt;p&gt;An increased file count can lead to issues such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More requests&lt;/li&gt;
&lt;li&gt;More complex catalog and dependency resolution&lt;/li&gt;
&lt;li&gt;A large number of tiny Bundles&lt;/li&gt;
&lt;li&gt;Higher file lookup and management cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this reason, label design and Bundle splitting should always be considered together.&lt;/p&gt;

&lt;p&gt;For example, assets like ADV or visual-novel-style scenario files often have many small files.&lt;/p&gt;

&lt;p&gt;In that case, splitting every scenario text file, small JSON file, or small configuration file into its own Bundle can create too many files and directly increase download or loading overhead.&lt;/p&gt;

&lt;p&gt;For this kind of data, it is often more stable in practice to group assets by chapter or event.&lt;/p&gt;

&lt;p&gt;On the other hand, assets like character 3D models are often added over time.&lt;/p&gt;

&lt;p&gt;For those, a design close to one Bundle per character may be more suitable.&lt;/p&gt;

&lt;p&gt;If characters are added, updated, or removed on a per-character basis, it is easier to operate when they can be downloaded in that same unit.&lt;/p&gt;

&lt;p&gt;I will cover this in more detail in the next article.&lt;/p&gt;


&lt;h2&gt;
  
  
  Manual label management is error-prone
&lt;/h2&gt;

&lt;p&gt;For small projects, manually managing Addressables from the Addressables Groups window is usually fine.&lt;/p&gt;

&lt;p&gt;However, as the number of assets grows, manual management becomes painful.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Forgetting to assign labels&lt;/li&gt;
&lt;li&gt;Inconsistent label naming&lt;/li&gt;
&lt;li&gt;Old labels remaining unintentionally&lt;/li&gt;
&lt;li&gt;Address naming rules becoming inconsistent&lt;/li&gt;
&lt;li&gt;Assets being placed in unintended Groups&lt;/li&gt;
&lt;li&gt;Forgetting to clean up assets after an event ends&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This becomes especially problematic in live-service games, where assets increase with each event or new character.&lt;/p&gt;

&lt;p&gt;If every Address and Label is managed manually in that situation, mistakes are almost guaranteed to happen eventually.&lt;/p&gt;

&lt;p&gt;Addressables is powerful, but it can become hard to maintain if your operational rules are unclear.&lt;/p&gt;


&lt;h2&gt;
  
  
  Use Smart Addresser
&lt;/h2&gt;

&lt;p&gt;If you want to manage Address and Label settings through rules, a tool like &lt;a href="https://github.com/CyberAgentGameEntertainment/SmartAddresser" rel="noopener noreferrer"&gt;Smart Addresser&lt;/a&gt; can help a lot.&lt;/p&gt;

&lt;p&gt;Smart Addresser is an Addressables support tool published by CyberAgentGameEntertainment.&lt;/p&gt;

&lt;p&gt;It is not an official Addressables feature, but it can automate management through rules, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic Address assignment&lt;/li&gt;
&lt;li&gt;Automatic Label assignment&lt;/li&gt;
&lt;li&gt;Version management&lt;/li&gt;
&lt;li&gt;Rule validation&lt;/li&gt;
&lt;li&gt;CLI-based application&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Manual management is enough for small projects.&lt;/p&gt;

&lt;p&gt;However, once the number of assets grows, it is safer to generate Address and Label settings from rules instead of assigning them by hand.&lt;/p&gt;

&lt;p&gt;In team development, it is especially important to make sure the same Address and Label settings are generated no matter who works on the assets.&lt;/p&gt;


&lt;h2&gt;
  
  
  Recommended practical setup
&lt;/h2&gt;

&lt;p&gt;In practice, the following setup is easy to operate.&lt;/p&gt;
&lt;h3&gt;
  
  
  Configure concurrent downloads at startup
&lt;/h3&gt;

&lt;p&gt;Call &lt;code&gt;WebRequestQueue.SetMaxConcurrentRequests()&lt;/code&gt; when the app starts.&lt;/p&gt;

&lt;p&gt;Use a fixed value for iOS, and switch based on device performance for Android, depending on your project.&lt;/p&gt;
&lt;h3&gt;
  
  
  Assign the &lt;code&gt;preload&lt;/code&gt; label only to the minimum required assets
&lt;/h3&gt;

&lt;p&gt;Include only the assets required for first launch or the title screen in &lt;code&gt;preload&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you put too many assets into this label, the initial download becomes heavy.&lt;/p&gt;

&lt;p&gt;It is better to keep &lt;code&gt;preload&lt;/code&gt; limited to assets that are absolutely required at the beginning.&lt;/p&gt;
&lt;h3&gt;
  
  
  Split labels by chapter, event, or character
&lt;/h3&gt;

&lt;p&gt;Design labels based on the loading flow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scenario_chapter_01
scenario_chapter_02
event_2026_summer
character_hero_001
voice_jp_chapter_01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Labels are easier to manage when they match the actual game progression.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check the download size before downloading
&lt;/h3&gt;

&lt;p&gt;Before a bulk download, use &lt;code&gt;GetDownloadSizeAsync()&lt;/code&gt; to check the size.&lt;/p&gt;

&lt;p&gt;If the size is large, you can show a confirmation dialog or recommend Wi-Fi.&lt;/p&gt;

&lt;p&gt;If the assets are already cached, the size will be &lt;code&gt;0&lt;/code&gt;, so you can skip unnecessary download processing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start larger downloads first when downloading multiple targets
&lt;/h3&gt;

&lt;p&gt;If you manage your own download queue for multiple labels or Bundles, start from the largest one.&lt;/p&gt;

&lt;p&gt;This helps avoid the situation where only a huge Bundle remains at the end, reducing the feeling that progress has stalled near the end of the loading screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design labels and Bundle splitting together
&lt;/h3&gt;

&lt;p&gt;Even if your labels are cleanly separated, the actual download unit stays large if the Bundle is large.&lt;/p&gt;

&lt;p&gt;On the other hand, splitting Bundles too finely increases the number of files.&lt;/p&gt;

&lt;p&gt;That is why labels and Bundle splitting should be designed together.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;When designing downloads with Addressables, the number of concurrent downloads and label design are important.&lt;/p&gt;

&lt;p&gt;Simply calling &lt;code&gt;DownloadDependenciesAsync&lt;/code&gt; does not determine download speed or operational stability.&lt;/p&gt;

&lt;p&gt;The number of concurrent downloads should not be increased blindly.&lt;/p&gt;

&lt;p&gt;It needs to be tuned based on device performance, network conditions, and Bundle size.&lt;/p&gt;

&lt;p&gt;In my projects, I use 10 on iOS and either 3 or 10 on Android depending on device performance, based on real-device testing.&lt;/p&gt;

&lt;p&gt;Also, when you manage your own queue for multiple download targets, starting from larger Bundles first helps avoid leaving one huge file until the end.&lt;/p&gt;

&lt;p&gt;Labels make bulk downloads easy.&lt;/p&gt;

&lt;p&gt;However, labels are only keys used to select download targets.&lt;/p&gt;

&lt;p&gt;The actual download unit is the AssetBundle.&lt;/p&gt;

&lt;p&gt;For that reason, label design and Bundle splitting should be considered together.&lt;/p&gt;

&lt;p&gt;As the number of assets grows, manual Address and Label management becomes more error-prone.&lt;/p&gt;

&lt;p&gt;In that case, using a rule-based management tool such as Smart Addresser can make operations much easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next: Bundle splitting and LZ4/LZMA
&lt;/h2&gt;

&lt;p&gt;In the next article, I will cover Addressables Bundle splitting and compression settings.&lt;/p&gt;

&lt;p&gt;In particular, I plan to cover topics such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether many small files, such as ADV scenario files, should be grouped together&lt;/li&gt;
&lt;li&gt;Whether character 3D models that will increase over time should use one Bundle per character&lt;/li&gt;
&lt;li&gt;Download and lookup overhead caused by increasing the number of files&lt;/li&gt;
&lt;li&gt;The downsides of splitting Bundles too finely&lt;/li&gt;
&lt;li&gt;How to choose between LZ4 and LZMA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bundle splitting is one of the areas where practical Addressables operation differs greatly between projects.&lt;/p&gt;

&lt;p&gt;Next time, I will organize more concrete decision criteria.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>addressables</category>
      <category>assetbundle</category>
    </item>
    <item>
      <title>Fast File Loading Techniques in Unity</title>
      <dc:creator>GameDevToolLab</dc:creator>
      <pubDate>Fri, 19 Jun 2026 11:26:02 +0000</pubDate>
      <link>https://dev.to/gamedevtoollab/fast-file-loading-techniques-in-unity-2131</link>
      <guid>https://dev.to/gamedevtoollab/fast-file-loading-techniques-in-unity-2131</guid>
      <description>&lt;h2&gt;
  
  
  Temporarily Switch to 60 FPS During Loading and Load Multiple Files in Parallel
&lt;/h2&gt;

&lt;p&gt;Even if your main gameplay runs at 30 FPS or another frame rate lower than 60 FPS, temporarily switching to 60 FPS during loading can improve loading times when reading multiple files.&lt;/p&gt;

&lt;p&gt;Also, when loading multiple files, it is more efficient to start all load operations first and then wait for all of them to finish, rather than waiting for each one sequentially.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Threading.Tasks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoadSample&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;async&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="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;prevFps&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;targetFrameRate&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;prevVSync&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;vSyncCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Switch to 60 FPS only during loading&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;vSyncCount&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;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;targetFrameRate&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;// Start multiple load operations at the same time&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;playerTask&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;GameObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"Player"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Task&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;enemyTask&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;GameObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"Enemy"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;uiTask&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;GameObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"BattleUI"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Wait until all load operations are complete&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enemyTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uiTask&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;playerPrefab&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;playerTask&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;enemyPrefab&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enemyTask&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;uiPrefab&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uiTask&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="c1"&gt;// Restore the FPS settings after loading is complete&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;targetFrameRate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prevFps&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;vSyncCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prevVSync&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nf"&gt;Instantiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerPrefab&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;Instantiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enemyPrefab&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;Instantiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uiPrefab&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;There are two key points.&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;// Start everything first&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;taskA&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;LoadA&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;taskB&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;LoadB&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;taskC&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;LoadC&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Then wait for all of them at the end&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taskA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taskB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taskC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you load them sequentially, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadA&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadB&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadC&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the waiting time accumulates.&lt;/p&gt;

&lt;p&gt;On the other hand, if the load operations do not depend on each other, starting them at the same time and waiting for all of them together may reduce the total loading time.&lt;/p&gt;

&lt;p&gt;However, loading many large assets at the same time can increase memory usage or cause spikes during asset expansion, so it is better to group them reasonably by screen or use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run Network Requests and File Loading at the Same Time
&lt;/h2&gt;

&lt;p&gt;When transitioning to a specific screen, you may need both of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetch data from a network API&lt;/li&gt;
&lt;li&gt;Load the prefab used to build the screen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you load the prefab only after the network request finishes, the waiting time is added together.&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;apiResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;CallApi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;prefab&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadPrefab&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 network request and the loading process do not depend on each other, it is more efficient to start them at the same time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;apiTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CallApi&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;prefabTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;LoadPrefab&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefabTask&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;apiResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;apiTask&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;prefab&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prefabTask&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the waiting time for the network request and the file loading overlaps, you can reduce the amount of time the user has to wait.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>performance</category>
      <category>addressable</category>
    </item>
  </channel>
</rss>
